mailtrap 2.8.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/Gemfile.lock +1 -1
- data/README.md +2 -0
- data/lib/mailtrap/email_log_event.rb +18 -0
- data/lib/mailtrap/email_log_event_details.rb +61 -0
- data/lib/mailtrap/email_log_message.rb +43 -0
- data/lib/mailtrap/email_logs_api.rb +135 -0
- data/lib/mailtrap/email_logs_list_response.rb +15 -0
- data/lib/mailtrap/mail.rb +3 -1
- data/lib/mailtrap/sandbox_messages_api.rb +20 -0
- data/lib/mailtrap/sending_stat_group.rb +15 -0
- data/lib/mailtrap/sending_stats.rb +30 -0
- data/lib/mailtrap/stats_api.rb +151 -0
- data/lib/mailtrap/version.rb +1 -1
- data/lib/mailtrap.rb +2 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3a95002cbfb65622fa2d944bf2de1d41dc25a9a7224f8d244982fdf8154776d
|
|
4
|
+
data.tar.gz: a228f3805ce687e8f34bd410595f6e39f9e7d73a426d1d06ca2b35ec972b5c69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fcec8536fae46762bd7289b7b098f45728c9d00b79bcd1386b6e4cb384fa0fcf859136db5cc956480da7aa1cb5063ed8dbee2312ac0e2bf6c8ac4b872443e650
|
|
7
|
+
data.tar.gz: 127126f80cded1f05214a173b1c8e78b8acc6c39bdbf1f168b161ca221a5e4230aa11d098f26c8c188c14778a28c579ac0e2bea52bab2713c82068d43836b677
|
data/CHANGELOG.md
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
|
+
## [2.10.0] - 2026-03-23
|
|
2
|
+
|
|
3
|
+
- Add Email Logs API (list and get email sending logs with filters and cursor pagination)
|
|
4
|
+
- Add `list_each` to Sandbox Messages API for automatic pagination over all messages
|
|
5
|
+
|
|
6
|
+
## [2.9.0] - 2026-03-13
|
|
7
|
+
|
|
8
|
+
- Add Sending Stats API
|
|
9
|
+
- Parse `reply_to` as a structured address field instead of passing it as a raw
|
|
10
|
+
header when sending with Action Mailer
|
|
11
|
+
|
|
1
12
|
## [2.8.0] - 2026-03-03
|
|
13
|
+
|
|
2
14
|
- Add Account Accesses API
|
|
3
15
|
- Add Billing API
|
|
4
16
|
|
|
5
|
-
## [2.7.0] - 2026-02-24
|
|
17
|
+
## [2.7.0] - 2026-02-24
|
|
18
|
+
|
|
6
19
|
- Add Sandbox Messages API
|
|
7
20
|
- Add Sending Domains API
|
|
8
21
|
- Add Sandbox Attachments API
|
|
9
22
|
- Add Accounts API
|
|
10
23
|
|
|
11
24
|
## [2.6.0] - 2026-01-27
|
|
25
|
+
|
|
12
26
|
- Add Inboxes API
|
|
13
27
|
- Add Projects API
|
|
14
28
|
- Models' `to_h` now returns all fields without compacting
|
|
15
29
|
|
|
16
30
|
## [2.5.0] - 2025-11-10
|
|
31
|
+
|
|
17
32
|
- Add Contact Imports API
|
|
18
33
|
- Add Suppressions API
|
|
19
34
|
- Write the message IDs to the message when sending with Action Mailer
|
|
20
35
|
- Fix versioning :)
|
|
21
36
|
|
|
22
37
|
## [2.4.1] - 2025-08-21
|
|
38
|
+
|
|
23
39
|
- Set `template_uuid` and `template_variables` when building mail from `Mail::Message`
|
|
24
40
|
|
|
25
41
|
## [2.4.0] - 2025-08-04
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -177,6 +177,8 @@ Email API:
|
|
|
177
177
|
- Full Email Sending – [`full.rb`](examples/full.rb)
|
|
178
178
|
- Batch Sending – [`batch.rb`](examples/batch.rb)
|
|
179
179
|
- Sending Domains API – [`sending_domains_api.rb`](examples/sending_domains_api.rb)
|
|
180
|
+
- Sending Stats API – [`stats_api.rb`](examples/stats_api.rb)
|
|
181
|
+
- Email Logs API – [`email_logs_api.rb`](examples/email_logs_api.rb)
|
|
180
182
|
|
|
181
183
|
Email Sandbox (Testing):
|
|
182
184
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailtrap
|
|
4
|
+
# Data Transfer Object for an email log event (delivery, open, click, bounce, etc.)
|
|
5
|
+
# @see https://docs.mailtrap.io/developers/email-sending/email-logs
|
|
6
|
+
# @attr_reader event_type [String] One of: delivery, open, click, soft_bounce, bounce, spam, unsubscribe, suspension,
|
|
7
|
+
# reject
|
|
8
|
+
# @attr_reader created_at [String] ISO 8601 timestamp
|
|
9
|
+
# @attr_reader details [EmailLogEventDetails::Delivery, EmailLogEventDetails::Open, EmailLogEventDetails::Click,
|
|
10
|
+
# EmailLogEventDetails::Bounce, EmailLogEventDetails::Spam, EmailLogEventDetails::Unsubscribe,
|
|
11
|
+
# EmailLogEventDetails::Reject] Type-specific event details
|
|
12
|
+
EmailLogEvent = Struct.new(
|
|
13
|
+
:event_type,
|
|
14
|
+
:created_at,
|
|
15
|
+
:details,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
)
|
|
18
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailtrap
|
|
4
|
+
# Type-specific event detail structs for EmailLogEvent. Use event_type to determine which details schema applies.
|
|
5
|
+
# @see https://docs.mailtrap.io/developers/email-sending/email-logs
|
|
6
|
+
module EmailLogEventDetails
|
|
7
|
+
# For event_type = delivery
|
|
8
|
+
Delivery = Struct.new(:sending_ip, :recipient_mx, :email_service_provider, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# For event_type = open
|
|
11
|
+
Open = Struct.new(:web_ip_address, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
# For event_type = click
|
|
14
|
+
Click = Struct.new(:click_url, :web_ip_address, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
# For event_type = soft_bounce or bounce
|
|
17
|
+
Bounce = Struct.new(
|
|
18
|
+
:sending_ip,
|
|
19
|
+
:recipient_mx,
|
|
20
|
+
:email_service_provider,
|
|
21
|
+
:email_service_provider_status,
|
|
22
|
+
:email_service_provider_response,
|
|
23
|
+
:bounce_category,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# For event_type = spam
|
|
28
|
+
Spam = Struct.new(:spam_feedback_type, keyword_init: true)
|
|
29
|
+
|
|
30
|
+
# For event_type = unsubscribe
|
|
31
|
+
Unsubscribe = Struct.new(:web_ip_address, keyword_init: true)
|
|
32
|
+
|
|
33
|
+
# For event_type = suspension or reject
|
|
34
|
+
Reject = Struct.new(:reject_reason, keyword_init: true)
|
|
35
|
+
|
|
36
|
+
DETAIL_STRUCTS = {
|
|
37
|
+
'delivery' => Delivery,
|
|
38
|
+
'open' => Open,
|
|
39
|
+
'click' => Click,
|
|
40
|
+
'soft_bounce' => Bounce,
|
|
41
|
+
'bounce' => Bounce,
|
|
42
|
+
'spam' => Spam,
|
|
43
|
+
'unsubscribe' => Unsubscribe,
|
|
44
|
+
'suspension' => Reject,
|
|
45
|
+
'reject' => Reject
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# Builds the appropriate detail struct from API response.
|
|
49
|
+
# @param event_type [String] Known event type (delivery, open, click, etc.)
|
|
50
|
+
# @param hash [Hash] Symbol-keyed details from parsed JSON
|
|
51
|
+
# @return [Delivery, Open, Click, Bounce, Spam, Unsubscribe, Reject]
|
|
52
|
+
# @raise [ArgumentError] when event_type is nil or not in DETAIL_STRUCTS
|
|
53
|
+
def self.build(event_type, hash)
|
|
54
|
+
struct_class = DETAIL_STRUCTS[event_type.to_s]
|
|
55
|
+
raise ArgumentError, "Unknown event_type: #{event_type.inspect}" unless struct_class
|
|
56
|
+
|
|
57
|
+
attrs = hash.slice(*struct_class.members)
|
|
58
|
+
struct_class.new(**attrs)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailtrap
|
|
4
|
+
# Data Transfer Object for an email log message (summary in list, full details when fetched by ID)
|
|
5
|
+
# @see https://docs.mailtrap.io/developers/email-sending/email-logs
|
|
6
|
+
# @attr_reader message_id [String] Message UUID
|
|
7
|
+
# @attr_reader status [String] delivered, not_delivered, enqueued, opted_out
|
|
8
|
+
# @attr_reader subject [String, nil] Email subject
|
|
9
|
+
# @attr_reader from [String] Sender address
|
|
10
|
+
# @attr_reader to [String] Recipient address
|
|
11
|
+
# @attr_reader sent_at [String] ISO 8601 timestamp
|
|
12
|
+
# @attr_reader client_ip [String, nil] Client IP that sent the email
|
|
13
|
+
# @attr_reader category [String, nil] Message category
|
|
14
|
+
# @attr_reader custom_variables [Hash, nil] Custom variables
|
|
15
|
+
# @attr_reader sending_stream [String] transactional or bulk
|
|
16
|
+
# @attr_reader sending_domain_id [Integer] Sending domain ID
|
|
17
|
+
# @attr_reader template_id [Integer, nil] Template ID if sent from template
|
|
18
|
+
# @attr_reader template_variables [Hash, nil] Template variables
|
|
19
|
+
# @attr_reader opens_count [Integer] Number of opens
|
|
20
|
+
# @attr_reader clicks_count [Integer] Number of clicks
|
|
21
|
+
# @attr_reader raw_message_url [String, nil] Signed URL to download raw .eml (only when fetched by ID)
|
|
22
|
+
# @attr_reader events [Array<EmailLogEvent>, nil] Event list (only when fetched by ID)
|
|
23
|
+
EmailLogMessage = Struct.new(
|
|
24
|
+
:message_id,
|
|
25
|
+
:status,
|
|
26
|
+
:subject,
|
|
27
|
+
:from,
|
|
28
|
+
:to,
|
|
29
|
+
:sent_at,
|
|
30
|
+
:client_ip,
|
|
31
|
+
:category,
|
|
32
|
+
:custom_variables,
|
|
33
|
+
:sending_stream,
|
|
34
|
+
:sending_domain_id,
|
|
35
|
+
:template_id,
|
|
36
|
+
:template_variables,
|
|
37
|
+
:opens_count,
|
|
38
|
+
:clicks_count,
|
|
39
|
+
:raw_message_url,
|
|
40
|
+
:events,
|
|
41
|
+
keyword_init: true
|
|
42
|
+
)
|
|
43
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_api'
|
|
4
|
+
require_relative 'email_log_message'
|
|
5
|
+
require_relative 'email_log_event'
|
|
6
|
+
require_relative 'email_log_event_details'
|
|
7
|
+
require_relative 'email_logs_list_response'
|
|
8
|
+
|
|
9
|
+
module Mailtrap
|
|
10
|
+
class EmailLogsAPI
|
|
11
|
+
include BaseAPI
|
|
12
|
+
|
|
13
|
+
self.response_class = EmailLogMessage
|
|
14
|
+
|
|
15
|
+
# Lists email logs with optional filters and cursor-based pagination.
|
|
16
|
+
#
|
|
17
|
+
# @param filters [Hash, nil] Optional filters. Top-level date keys use string values (ISO 8601);
|
|
18
|
+
# other keys use +{ operator:, value: }+. +value+ can be a single value or an Array for
|
|
19
|
+
# operators that accept multiple values (e.g. +equal+, +not_equal+, +ci_equal+, +ci_not_equal+).
|
|
20
|
+
# Examples:
|
|
21
|
+
# +{ sent_after: "2025-01-01T00:00:00Z", sent_before: "2025-01-31T23:59:59Z" }+
|
|
22
|
+
# +{ to: { operator: "ci_equal", value: "recipient@example.com" } }+
|
|
23
|
+
# +{ category: { operator: "equal", value: ["Welcome Email", "Transactional Email"] } }+
|
|
24
|
+
# @param search_after [String, nil] Message UUID cursor for the next page (from previous +next_page_cursor+)
|
|
25
|
+
# @return [EmailLogsListResponse] messages, total_count, and next_page_cursor
|
|
26
|
+
# @!macro api_errors
|
|
27
|
+
def list(filters: nil, search_after: nil)
|
|
28
|
+
query_params = build_list_query_params(filters:, search_after:)
|
|
29
|
+
|
|
30
|
+
response = client.get(base_path, query_params)
|
|
31
|
+
|
|
32
|
+
build_list_response(response)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Iterates over all email log messages matching the filters, automatically fetching each page.
|
|
36
|
+
# Use this when you want to process every message without manually handling +next_page_cursor+.
|
|
37
|
+
#
|
|
38
|
+
# @param filters [Hash, nil] Same as +list+
|
|
39
|
+
# @yield [EmailLogMessage] Gives each message from every page when a block is given.
|
|
40
|
+
# @return [Enumerator<EmailLogMessage>] if no block given; otherwise the result of the block
|
|
41
|
+
# @!macro api_errors
|
|
42
|
+
def list_each(filters: nil, &block)
|
|
43
|
+
first_page = nil
|
|
44
|
+
fetch_first_page = -> { first_page ||= list(filters: filters) }
|
|
45
|
+
enum = Enumerator.new(-> { fetch_first_page.call.total_count }) do |yielder|
|
|
46
|
+
response = fetch_first_page.call
|
|
47
|
+
loop do
|
|
48
|
+
response.messages.each { |message| yielder << message }
|
|
49
|
+
break if response.next_page_cursor.nil?
|
|
50
|
+
|
|
51
|
+
response = list(filters: filters, search_after: response.next_page_cursor)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
block ? enum.each(&block) : enum
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Fetches a single email log message by ID.
|
|
59
|
+
#
|
|
60
|
+
# @param sending_message_id [String] Message UUID
|
|
61
|
+
# @return [EmailLogMessage] Message with events and raw_message_url when available
|
|
62
|
+
# @!macro api_errors
|
|
63
|
+
def get(sending_message_id)
|
|
64
|
+
base_get(sending_message_id)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def base_path
|
|
70
|
+
"/api/accounts/#{account_id}/email_logs"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_list_query_params(filters:, search_after:)
|
|
74
|
+
{}.tap do |params|
|
|
75
|
+
params[:search_after] = search_after if search_after
|
|
76
|
+
params.merge!(flatten_filters(filters))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Flattens a filters Hash into query param keys expected by the API (deepObject style).
|
|
81
|
+
# Scalar values => filters[key]; Hashes with :operator/:value => filters[key][operator], filters[key][value].
|
|
82
|
+
# When :value is an Array, the key is repeated (e.g. filters[category][value]=A&filters[category][value]=B)
|
|
83
|
+
# for operators that accept multiple values (e.g. equal, not_equal, ci_equal, ci_not_equal).
|
|
84
|
+
def flatten_filters(filters)
|
|
85
|
+
return {} if filters.nil? || filters.empty?
|
|
86
|
+
|
|
87
|
+
filters.each_with_object({}) do |(key, value), result|
|
|
88
|
+
if value.is_a?(Hash)
|
|
89
|
+
flatten_filter_hash(key, value, result)
|
|
90
|
+
else
|
|
91
|
+
result["filters[#{key}]"] = value.to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def flatten_filter_hash(parent_key, hash, result)
|
|
97
|
+
hash.each do |key, value|
|
|
98
|
+
if value.is_a?(Array)
|
|
99
|
+
result["filters[#{parent_key}][#{key}][]"] = value.map(&:to_s)
|
|
100
|
+
else
|
|
101
|
+
result["filters[#{parent_key}][#{key}]"] = value.to_s
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_list_response(response)
|
|
107
|
+
EmailLogsListResponse.new(
|
|
108
|
+
messages: Array(response[:messages]).map { |item| handle_response(item) },
|
|
109
|
+
total_count: response[:total_count],
|
|
110
|
+
next_page_cursor: response[:next_page_cursor]
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_response(response)
|
|
115
|
+
build_message_entity(response)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_message_entity(hash)
|
|
119
|
+
attrs = hash.slice(*EmailLogMessage.members)
|
|
120
|
+
attrs[:events] = build_events(attrs[:events]) if attrs[:events]
|
|
121
|
+
|
|
122
|
+
EmailLogMessage.new(**attrs)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_events(events_array)
|
|
126
|
+
Array(events_array).map do |e|
|
|
127
|
+
EmailLogEvent.new(
|
|
128
|
+
event_type: e[:event_type],
|
|
129
|
+
created_at: e[:created_at],
|
|
130
|
+
details: EmailLogEventDetails.build(e[:event_type], e[:details])
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailtrap
|
|
4
|
+
# Response from listing email logs (paginated)
|
|
5
|
+
# @see https://docs.mailtrap.io/developers/email-sending/email-logs
|
|
6
|
+
# @attr_reader messages [Array<EmailLogMessage>] Page of message summaries
|
|
7
|
+
# @attr_reader total_count [Integer] Total number of messages matching filters
|
|
8
|
+
# @attr_reader next_page_cursor [String, nil] Message UUID to use as search_after for next page, or nil
|
|
9
|
+
EmailLogsListResponse = Struct.new(
|
|
10
|
+
:messages,
|
|
11
|
+
:total_count,
|
|
12
|
+
:next_page_cursor,
|
|
13
|
+
keyword_init: true
|
|
14
|
+
)
|
|
15
|
+
end
|
data/lib/mailtrap/mail.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Mailtrap
|
|
|
17
17
|
category
|
|
18
18
|
customvariables
|
|
19
19
|
contenttype
|
|
20
|
+
replyto
|
|
20
21
|
].freeze
|
|
21
22
|
private_constant :SPECIAL_HEADERS
|
|
22
23
|
|
|
@@ -189,12 +190,13 @@ module Mailtrap
|
|
|
189
190
|
# Builds a mail object from Mail::Message
|
|
190
191
|
# @param message [Mail::Message]
|
|
191
192
|
# @return [Mailtrap::Mail::Base]
|
|
192
|
-
def from_message(message)
|
|
193
|
+
def from_message(message) # rubocop:disable Metrics/AbcSize
|
|
193
194
|
Mailtrap::Mail::Base.new(
|
|
194
195
|
from: prepare_addresses(message['from']).first,
|
|
195
196
|
to: prepare_addresses(message['to']),
|
|
196
197
|
cc: prepare_addresses(message['cc']),
|
|
197
198
|
bcc: prepare_addresses(message['bcc']),
|
|
199
|
+
reply_to: prepare_addresses(message['reply-to']).first,
|
|
198
200
|
subject: message.subject,
|
|
199
201
|
text: prepare_text_part(message),
|
|
200
202
|
html: prepare_html_part(message),
|
|
@@ -51,6 +51,26 @@ module Mailtrap
|
|
|
51
51
|
base_update(message_id, { is_read: is_read })
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Iterates over all sandbox messages, automatically fetching each page
|
|
55
|
+
# using cursor-based pagination. Use this when you want to process messages
|
|
56
|
+
# without manually handling pagination.
|
|
57
|
+
# @param search [String] Search query string. Matches subject, to_email, and to_name.
|
|
58
|
+
# @yield [SandboxMessage] Gives each message from every page when a block is given.
|
|
59
|
+
# @return [Enumerator<SandboxMessage>] if no block given; otherwise the result of the block
|
|
60
|
+
# @!macro api_errors
|
|
61
|
+
def list_each(search: nil, &block)
|
|
62
|
+
return to_enum(__method__, search: search) unless block
|
|
63
|
+
|
|
64
|
+
last_id = nil
|
|
65
|
+
loop do
|
|
66
|
+
messages = list(search: search, last_id: last_id)
|
|
67
|
+
break if messages.empty?
|
|
68
|
+
|
|
69
|
+
messages.each { |message| block.call(message) }
|
|
70
|
+
last_id = messages.last.id
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
54
74
|
# Lists all sandbox messages for the account, limited up to 30 at once
|
|
55
75
|
# @param search [String] Search query string. Matches subject, to_email, and to_name.
|
|
56
76
|
# @param last_id [Integer] If specified, a page of records before last_id is returned.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailtrap
|
|
4
|
+
# Data Transfer Object for grouped Sending Stats data
|
|
5
|
+
# @attr_reader name [Symbol] Group type (:category, :date, :sending_domain_id, :email_service_provider)
|
|
6
|
+
# @attr_reader value [String, Integer] Group value (e.g., "Transactional", "2026-01-01", 1, "Gmail")
|
|
7
|
+
# @attr_reader stats [SendingStats] Sending stats for this group
|
|
8
|
+
#
|
|
9
|
+
SendingStatGroup = Struct.new(
|
|
10
|
+
:name,
|
|
11
|
+
:value,
|
|
12
|
+
:stats,
|
|
13
|
+
keyword_init: true
|
|
14
|
+
)
|
|
15
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mailtrap
|
|
4
|
+
# Data Transfer Object for Sending Stats data
|
|
5
|
+
# @see https://docs.mailtrap.io/developers/email-sending-stats
|
|
6
|
+
# @attr_reader delivery_count [Integer] Number of delivered emails
|
|
7
|
+
# @attr_reader delivery_rate [Float] Delivery rate
|
|
8
|
+
# @attr_reader bounce_count [Integer] Number of bounced emails
|
|
9
|
+
# @attr_reader bounce_rate [Float] Bounce rate
|
|
10
|
+
# @attr_reader open_count [Integer] Number of opened emails
|
|
11
|
+
# @attr_reader open_rate [Float] Open rate
|
|
12
|
+
# @attr_reader click_count [Integer] Number of clicked emails
|
|
13
|
+
# @attr_reader click_rate [Float] Click rate
|
|
14
|
+
# @attr_reader spam_count [Integer] Number of spam reports
|
|
15
|
+
# @attr_reader spam_rate [Float] Spam rate
|
|
16
|
+
#
|
|
17
|
+
SendingStats = Struct.new(
|
|
18
|
+
:delivery_count,
|
|
19
|
+
:delivery_rate,
|
|
20
|
+
:bounce_count,
|
|
21
|
+
:bounce_rate,
|
|
22
|
+
:open_count,
|
|
23
|
+
:open_rate,
|
|
24
|
+
:click_count,
|
|
25
|
+
:click_rate,
|
|
26
|
+
:spam_count,
|
|
27
|
+
:spam_rate,
|
|
28
|
+
keyword_init: true
|
|
29
|
+
)
|
|
30
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require_relative 'base_api'
|
|
5
|
+
require_relative 'sending_stats'
|
|
6
|
+
require_relative 'sending_stat_group'
|
|
7
|
+
|
|
8
|
+
module Mailtrap
|
|
9
|
+
class StatsAPI
|
|
10
|
+
include BaseAPI
|
|
11
|
+
|
|
12
|
+
ARRAY_FILTERS = %i[sending_domain_ids sending_streams categories email_service_providers].freeze
|
|
13
|
+
GROUP_KEYS = {
|
|
14
|
+
'domains' => :sending_domain_id,
|
|
15
|
+
'categories' => :category,
|
|
16
|
+
'email_service_providers' => :email_service_provider,
|
|
17
|
+
'date' => :date
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Get aggregated sending stats
|
|
21
|
+
# @param start_date [String, Date, Time] Start date for the stats period (required)
|
|
22
|
+
# @param end_date [String, Date, Time] End date for the stats period (required)
|
|
23
|
+
# @param sending_domain_ids [Array<Integer>] Filter by sending domain IDs
|
|
24
|
+
# @param sending_streams [Array<String>] Filter by sending streams
|
|
25
|
+
# @param categories [Array<String>] Filter by categories
|
|
26
|
+
# @param email_service_providers [Array<String>] Filter by email service providers
|
|
27
|
+
# @return [SendingStats] Aggregated sending stats
|
|
28
|
+
# @!macro api_errors
|
|
29
|
+
def get(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists
|
|
30
|
+
email_service_providers: nil)
|
|
31
|
+
query_params = build_query_params(
|
|
32
|
+
start_date, end_date,
|
|
33
|
+
{ sending_domain_ids:, sending_streams:, categories:, email_service_providers: }
|
|
34
|
+
)
|
|
35
|
+
response = client.get(base_path, query_params)
|
|
36
|
+
build_entity(response, SendingStats)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get sending stats grouped by domain
|
|
40
|
+
# @param start_date [String, Date, Time] Start date for the stats period (required)
|
|
41
|
+
# @param end_date [String, Date, Time] End date for the stats period (required)
|
|
42
|
+
# @param sending_domain_ids [Array<Integer>] Filter by sending domain IDs
|
|
43
|
+
# @param sending_streams [Array<String>] Filter by sending streams
|
|
44
|
+
# @param categories [Array<String>] Filter by categories
|
|
45
|
+
# @param email_service_providers [Array<String>] Filter by email service providers
|
|
46
|
+
# @return [Array<SendingStatGroup>] Array of SendingStatGroup structs with sending_domain_id and stats
|
|
47
|
+
# @!macro api_errors
|
|
48
|
+
def by_domain(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists
|
|
49
|
+
email_service_providers: nil)
|
|
50
|
+
grouped_stats('domains', start_date, end_date,
|
|
51
|
+
{ sending_domain_ids:, sending_streams:, categories:, email_service_providers: })
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get sending stats grouped by category
|
|
55
|
+
# @param start_date [String, Date, Time] Start date for the stats period (required)
|
|
56
|
+
# @param end_date [String, Date, Time] End date for the stats period (required)
|
|
57
|
+
# @param sending_domain_ids [Array<Integer>] Filter by sending domain IDs
|
|
58
|
+
# @param sending_streams [Array<String>] Filter by sending streams
|
|
59
|
+
# @param categories [Array<String>] Filter by categories
|
|
60
|
+
# @param email_service_providers [Array<String>] Filter by email service providers
|
|
61
|
+
# @return [Array<SendingStatGroup>] Array of SendingStatGroup structs with category and stats
|
|
62
|
+
# @!macro api_errors
|
|
63
|
+
def by_category(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists
|
|
64
|
+
email_service_providers: nil)
|
|
65
|
+
grouped_stats('categories', start_date, end_date,
|
|
66
|
+
{ sending_domain_ids:, sending_streams:, categories:, email_service_providers: })
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get sending stats grouped by email service provider
|
|
70
|
+
# @param start_date [String, Date, Time] Start date for the stats period (required)
|
|
71
|
+
# @param end_date [String, Date, Time] End date for the stats period (required)
|
|
72
|
+
# @param sending_domain_ids [Array<Integer>] Filter by sending domain IDs
|
|
73
|
+
# @param sending_streams [Array<String>] Filter by sending streams
|
|
74
|
+
# @param categories [Array<String>] Filter by categories
|
|
75
|
+
# @param email_service_providers [Array<String>] Filter by email service providers
|
|
76
|
+
# @return [Array<SendingStatGroup>] Array of SendingStatGroup structs with email_service_provider and stats
|
|
77
|
+
# @!macro api_errors
|
|
78
|
+
def by_email_service_provider(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, # rubocop:disable Metrics/ParameterLists
|
|
79
|
+
categories: nil, email_service_providers: nil)
|
|
80
|
+
grouped_stats('email_service_providers', start_date, end_date,
|
|
81
|
+
{ sending_domain_ids:, sending_streams:, categories:, email_service_providers: })
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get sending stats grouped by date
|
|
85
|
+
# @param start_date [String, Date, Time] Start date for the stats period (required)
|
|
86
|
+
# @param end_date [String, Date, Time] End date for the stats period (required)
|
|
87
|
+
# @param sending_domain_ids [Array<Integer>] Filter by sending domain IDs
|
|
88
|
+
# @param sending_streams [Array<String>] Filter by sending streams
|
|
89
|
+
# @param categories [Array<String>] Filter by categories
|
|
90
|
+
# @param email_service_providers [Array<String>] Filter by email service providers
|
|
91
|
+
# @return [Array<SendingStatGroup>] Array of SendingStatGroup structs with date and stats
|
|
92
|
+
# @!macro api_errors
|
|
93
|
+
def by_date(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists
|
|
94
|
+
email_service_providers: nil)
|
|
95
|
+
grouped_stats('date', start_date, end_date,
|
|
96
|
+
{ sending_domain_ids:, sending_streams:, categories:, email_service_providers: })
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def grouped_stats(group, start_date, end_date, filters)
|
|
102
|
+
query_params = build_query_params(start_date, end_date, filters)
|
|
103
|
+
response = client.get("#{base_path}/#{group}", query_params)
|
|
104
|
+
group_key = GROUP_KEYS.fetch(group)
|
|
105
|
+
|
|
106
|
+
response.map do |item|
|
|
107
|
+
SendingStatGroup.new(
|
|
108
|
+
name: group_key,
|
|
109
|
+
value: item[group_key],
|
|
110
|
+
stats: build_entity(item[:stats], SendingStats)
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def build_query_params(start_date, end_date, filters)
|
|
116
|
+
params = { start_date: normalize_date(start_date), end_date: normalize_date(end_date) }
|
|
117
|
+
|
|
118
|
+
ARRAY_FILTERS.each do |filter_key|
|
|
119
|
+
values = filters[filter_key]
|
|
120
|
+
params["#{filter_key}[]"] = values if values
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
params
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def normalize_date(value)
|
|
127
|
+
case value
|
|
128
|
+
when Date
|
|
129
|
+
value.iso8601
|
|
130
|
+
when Time
|
|
131
|
+
value.strftime('%F')
|
|
132
|
+
when String
|
|
133
|
+
unless /\A\d{4}-\d{2}-\d{2}\z/.match?(value)
|
|
134
|
+
raise ArgumentError,
|
|
135
|
+
"Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format."
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
Date.iso8601(value).iso8601
|
|
139
|
+
else
|
|
140
|
+
raise ArgumentError,
|
|
141
|
+
"Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format."
|
|
142
|
+
end
|
|
143
|
+
rescue Date::Error
|
|
144
|
+
raise ArgumentError, "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format."
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def base_path
|
|
148
|
+
"/api/accounts/#{account_id}/stats"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/mailtrap/version.rb
CHANGED
data/lib/mailtrap.rb
CHANGED
|
@@ -14,10 +14,12 @@ require_relative 'mailtrap/contact_fields_api'
|
|
|
14
14
|
require_relative 'mailtrap/contact_imports_api'
|
|
15
15
|
require_relative 'mailtrap/suppressions_api'
|
|
16
16
|
require_relative 'mailtrap/sending_domains_api'
|
|
17
|
+
require_relative 'mailtrap/email_logs_api'
|
|
17
18
|
require_relative 'mailtrap/projects_api'
|
|
18
19
|
require_relative 'mailtrap/inboxes_api'
|
|
19
20
|
require_relative 'mailtrap/sandbox_messages_api'
|
|
20
21
|
require_relative 'mailtrap/sandbox_attachments_api'
|
|
22
|
+
require_relative 'mailtrap/stats_api'
|
|
21
23
|
|
|
22
24
|
module Mailtrap
|
|
23
25
|
# @!macro api_errors
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailtrap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Railsware Products Studio LLC
|
|
@@ -63,6 +63,11 @@ files:
|
|
|
63
63
|
- lib/mailtrap/contact_lists_api.rb
|
|
64
64
|
- lib/mailtrap/contacts_api.rb
|
|
65
65
|
- lib/mailtrap/contacts_import_request.rb
|
|
66
|
+
- lib/mailtrap/email_log_event.rb
|
|
67
|
+
- lib/mailtrap/email_log_event_details.rb
|
|
68
|
+
- lib/mailtrap/email_log_message.rb
|
|
69
|
+
- lib/mailtrap/email_logs_api.rb
|
|
70
|
+
- lib/mailtrap/email_logs_list_response.rb
|
|
66
71
|
- lib/mailtrap/email_template.rb
|
|
67
72
|
- lib/mailtrap/email_templates_api.rb
|
|
68
73
|
- lib/mailtrap/errors.rb
|
|
@@ -79,6 +84,9 @@ files:
|
|
|
79
84
|
- lib/mailtrap/sandbox_messages_api.rb
|
|
80
85
|
- lib/mailtrap/sending_domain.rb
|
|
81
86
|
- lib/mailtrap/sending_domains_api.rb
|
|
87
|
+
- lib/mailtrap/sending_stat_group.rb
|
|
88
|
+
- lib/mailtrap/sending_stats.rb
|
|
89
|
+
- lib/mailtrap/stats_api.rb
|
|
82
90
|
- lib/mailtrap/suppression.rb
|
|
83
91
|
- lib/mailtrap/suppressions_api.rb
|
|
84
92
|
- lib/mailtrap/version.rb
|