From 3933b2d04b7c4bdce1d1ffe7bc472375bf7406a0 Mon Sep 17 00:00:00 2001 From: Piotr Bednarz Date: Tue, 10 Mar 2026 12:40:04 +0100 Subject: [PATCH] Add Stats API --- CHANGELOG.md | 11 +- README.md | 1 + examples/stats_api.rb | 48 ++++ lib/mailtrap.rb | 1 + lib/mailtrap/sending_stat_group.rb | 15 ++ lib/mailtrap/sending_stats.rb | 30 +++ lib/mailtrap/stats_api.rb | 151 +++++++++++++ .../returns_stats_grouped_by_category.yml | 73 ++++++ .../returns_stats_grouped_by_date.yml | 73 ++++++ .../returns_stats_grouped_by_domain.yml | 73 ++++++ ...tats_grouped_by_email_service_provider.yml | 73 ++++++ .../_get/returns_aggregated_sending_stats.yml | 73 ++++++ .../raises_authorization_error.yml | 73 ++++++ .../returns_filtered_sending_stats.yml | 73 ++++++ spec/mailtrap/stats_api_spec.rb | 212 ++++++++++++++++++ spec/spec_helper.rb | 8 +- 16 files changed, 986 insertions(+), 2 deletions(-) create mode 100644 examples/stats_api.rb create mode 100644 lib/mailtrap/sending_stat_group.rb create mode 100644 lib/mailtrap/sending_stats.rb create mode 100644 lib/mailtrap/stats_api.rb create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_category/returns_stats_grouped_by_category.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_date/returns_stats_grouped_by_date.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_domain/returns_stats_grouped_by_domain.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_email_service_provider/returns_stats_grouped_by_email_service_provider.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/when_api_key_is_incorrect/raises_authorization_error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/with_optional_filters/returns_filtered_sending_stats.yml create mode 100644 spec/mailtrap/stats_api_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d690d11..5e5100f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,34 @@ +## Unreleased + +- Add Sending Stats API + ## [2.8.0] - 2026-03-03 + - Add Account Accesses API - Add Billing API -## [2.7.0] - 2026-02-24 +## [2.7.0] - 2026-02-24 + - Add Sandbox Messages API - Add Sending Domains API - Add Sandbox Attachments API - Add Accounts API ## [2.6.0] - 2026-01-27 + - Add Inboxes API - Add Projects API - Models' `to_h` now returns all fields without compacting ## [2.5.0] - 2025-11-10 + - Add Contact Imports API - Add Suppressions API - Write the message IDs to the message when sending with Action Mailer - Fix versioning :) ## [2.4.1] - 2025-08-21 + - Set `template_uuid` and `template_variables` when building mail from `Mail::Message` ## [2.4.0] - 2025-08-04 diff --git a/README.md b/README.md index b90693a..040004a 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ Email API: - Full Email Sending – [`full.rb`](examples/full.rb) - Batch Sending – [`batch.rb`](examples/batch.rb) - Sending Domains API – [`sending_domains_api.rb`](examples/sending_domains_api.rb) +- Sending Stats API – [`stats_api.rb`](examples/stats_api.rb) Email Sandbox (Testing): diff --git a/examples/stats_api.rb b/examples/stats_api.rb new file mode 100644 index 0000000..d6c700a --- /dev/null +++ b/examples/stats_api.rb @@ -0,0 +1,48 @@ +require 'mailtrap' +require 'date' +require 'time' + +account_id = 3229 +client = Mailtrap::Client.new(api_key: 'your-api-key') +stats = Mailtrap::StatsAPI.new(account_id, client) + +# Get aggregated sending stats +stats.get(start_date: '2026-01-01', end_date: '2026-01-31') +# => # + +# Get stats grouped by domain +stats.by_domain(start_date: '2026-01-01', end_date: '2026-01-31') +# => [#>, ...] + +# Get stats grouped by category +stats.by_category(start_date: Date.today.prev_day(30), end_date: Date.today) +# => [#>, ...] + +# Get stats grouped by email service provider +stats.by_email_service_provider(start_date: Time.new(2026, 1, 1), end_date: Time.new(2026, 1, 31)) +# => [#>, ...] + +# Get stats grouped by date +stats.by_date(start_date: '2026-01-01', end_date: '2026-01-31') +# => [#>, ...] + +# With optional filters +stats.get( + start_date: '2026-01-01', + end_date: '2026-01-31', + sending_domain_ids: [1, 2], + categories: ['Transactional'] +) diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index 76cde54..f6f5ea9 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -18,6 +18,7 @@ require_relative 'mailtrap/inboxes_api' require_relative 'mailtrap/sandbox_messages_api' require_relative 'mailtrap/sandbox_attachments_api' +require_relative 'mailtrap/stats_api' module Mailtrap # @!macro api_errors diff --git a/lib/mailtrap/sending_stat_group.rb b/lib/mailtrap/sending_stat_group.rb new file mode 100644 index 0000000..50ad07b --- /dev/null +++ b/lib/mailtrap/sending_stat_group.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for grouped Sending Stats data + # @attr_reader name [Symbol] Group type (:category, :date, :sending_domain_id, :email_service_provider) + # @attr_reader value [String, Integer] Group value (e.g., "Transactional", "2026-01-01", 1, "Gmail") + # @attr_reader stats [SendingStats] Sending stats for this group + # + SendingStatGroup = Struct.new( + :name, + :value, + :stats, + keyword_init: true + ) +end diff --git a/lib/mailtrap/sending_stats.rb b/lib/mailtrap/sending_stats.rb new file mode 100644 index 0000000..3c41d4b --- /dev/null +++ b/lib/mailtrap/sending_stats.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for Sending Stats data + # @see https://docs.mailtrap.io/developers/email-sending-stats + # @attr_reader delivery_count [Integer] Number of delivered emails + # @attr_reader delivery_rate [Float] Delivery rate + # @attr_reader bounce_count [Integer] Number of bounced emails + # @attr_reader bounce_rate [Float] Bounce rate + # @attr_reader open_count [Integer] Number of opened emails + # @attr_reader open_rate [Float] Open rate + # @attr_reader click_count [Integer] Number of clicked emails + # @attr_reader click_rate [Float] Click rate + # @attr_reader spam_count [Integer] Number of spam reports + # @attr_reader spam_rate [Float] Spam rate + # + SendingStats = Struct.new( + :delivery_count, + :delivery_rate, + :bounce_count, + :bounce_rate, + :open_count, + :open_rate, + :click_count, + :click_rate, + :spam_count, + :spam_rate, + keyword_init: true + ) +end diff --git a/lib/mailtrap/stats_api.rb b/lib/mailtrap/stats_api.rb new file mode 100644 index 0000000..dfdd8ef --- /dev/null +++ b/lib/mailtrap/stats_api.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'date' +require_relative 'base_api' +require_relative 'sending_stats' +require_relative 'sending_stat_group' + +module Mailtrap + class StatsAPI + include BaseAPI + + ARRAY_FILTERS = %i[sending_domain_ids sending_streams categories email_service_providers].freeze + GROUP_KEYS = { + 'domains' => :sending_domain_id, + 'categories' => :category, + 'email_service_providers' => :email_service_provider, + 'date' => :date + }.freeze + + # Get aggregated sending stats + # @param start_date [String, Date, Time] Start date for the stats period (required) + # @param end_date [String, Date, Time] End date for the stats period (required) + # @param sending_domain_ids [Array] Filter by sending domain IDs + # @param sending_streams [Array] Filter by sending streams + # @param categories [Array] Filter by categories + # @param email_service_providers [Array] Filter by email service providers + # @return [SendingStats] Aggregated sending stats + # @!macro api_errors + def get(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists + email_service_providers: nil) + query_params = build_query_params( + start_date, end_date, + { sending_domain_ids:, sending_streams:, categories:, email_service_providers: } + ) + response = client.get(base_path, query_params) + build_entity(response, SendingStats) + end + + # Get sending stats grouped by domain + # @param start_date [String, Date, Time] Start date for the stats period (required) + # @param end_date [String, Date, Time] End date for the stats period (required) + # @param sending_domain_ids [Array] Filter by sending domain IDs + # @param sending_streams [Array] Filter by sending streams + # @param categories [Array] Filter by categories + # @param email_service_providers [Array] Filter by email service providers + # @return [Array] Array of SendingStatGroup structs with sending_domain_id and stats + # @!macro api_errors + def by_domain(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists + email_service_providers: nil) + grouped_stats('domains', start_date, end_date, + { sending_domain_ids:, sending_streams:, categories:, email_service_providers: }) + end + + # Get sending stats grouped by category + # @param start_date [String, Date, Time] Start date for the stats period (required) + # @param end_date [String, Date, Time] End date for the stats period (required) + # @param sending_domain_ids [Array] Filter by sending domain IDs + # @param sending_streams [Array] Filter by sending streams + # @param categories [Array] Filter by categories + # @param email_service_providers [Array] Filter by email service providers + # @return [Array] Array of SendingStatGroup structs with category and stats + # @!macro api_errors + def by_category(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists + email_service_providers: nil) + grouped_stats('categories', start_date, end_date, + { sending_domain_ids:, sending_streams:, categories:, email_service_providers: }) + end + + # Get sending stats grouped by email service provider + # @param start_date [String, Date, Time] Start date for the stats period (required) + # @param end_date [String, Date, Time] End date for the stats period (required) + # @param sending_domain_ids [Array] Filter by sending domain IDs + # @param sending_streams [Array] Filter by sending streams + # @param categories [Array] Filter by categories + # @param email_service_providers [Array] Filter by email service providers + # @return [Array] Array of SendingStatGroup structs with email_service_provider and stats + # @!macro api_errors + def by_email_service_provider(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, # rubocop:disable Metrics/ParameterLists + categories: nil, email_service_providers: nil) + grouped_stats('email_service_providers', start_date, end_date, + { sending_domain_ids:, sending_streams:, categories:, email_service_providers: }) + end + + # Get sending stats grouped by date + # @param start_date [String, Date, Time] Start date for the stats period (required) + # @param end_date [String, Date, Time] End date for the stats period (required) + # @param sending_domain_ids [Array] Filter by sending domain IDs + # @param sending_streams [Array] Filter by sending streams + # @param categories [Array] Filter by categories + # @param email_service_providers [Array] Filter by email service providers + # @return [Array] Array of SendingStatGroup structs with date and stats + # @!macro api_errors + def by_date(start_date:, end_date:, sending_domain_ids: nil, sending_streams: nil, categories: nil, # rubocop:disable Metrics/ParameterLists + email_service_providers: nil) + grouped_stats('date', start_date, end_date, + { sending_domain_ids:, sending_streams:, categories:, email_service_providers: }) + end + + private + + def grouped_stats(group, start_date, end_date, filters) + query_params = build_query_params(start_date, end_date, filters) + response = client.get("#{base_path}/#{group}", query_params) + group_key = GROUP_KEYS.fetch(group) + + response.map do |item| + SendingStatGroup.new( + name: group_key, + value: item[group_key], + stats: build_entity(item[:stats], SendingStats) + ) + end + end + + def build_query_params(start_date, end_date, filters) + params = { start_date: normalize_date(start_date), end_date: normalize_date(end_date) } + + ARRAY_FILTERS.each do |filter_key| + values = filters[filter_key] + params["#{filter_key}[]"] = values if values + end + + params + end + + def normalize_date(value) + case value + when Date + value.iso8601 + when Time + value.strftime('%F') + when String + unless /\A\d{4}-\d{2}-\d{2}\z/.match?(value) + raise ArgumentError, + "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." + end + + Date.iso8601(value).iso8601 + else + raise ArgumentError, + "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." + end + rescue Date::Error + raise ArgumentError, "Invalid date: #{value.inspect}. Expected a Date, Time, or String in YYYY-MM-DD format." + end + + def base_path + "/api/accounts/#{account_id}/stats" + end + end +end diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_category/returns_stats_grouped_by_category.yml b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_category/returns_stats_grouped_by_category.yml new file mode 100644 index 0000000..5dcf89c --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_category/returns_stats_grouped_by_category.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/stats/categories?end_date=2026-01-31&start_date=2026-01-01 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Mar 2026 10:00:03 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"ghi789" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.028000' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"category":"Transactional","stats":{"delivery_count":100,"delivery_rate":0.97,"bounce_count":3,"bounce_rate":0.03,"open_count":85,"open_rate":0.85,"click_count":45,"click_rate":0.53,"spam_count":0,"spam_rate":0.0}},{"category":"Marketing","stats":{"delivery_count":50,"delivery_rate":0.91,"bounce_count":5,"bounce_rate":0.09,"open_count":35,"open_rate":0.7,"click_count":15,"click_rate":0.43,"spam_count":2,"spam_rate":0.04}}]' + recorded_at: Tue, 04 Mar 2026 10:00:03 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_date/returns_stats_grouped_by_date.yml b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_date/returns_stats_grouped_by_date.yml new file mode 100644 index 0000000..94396c8 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_date/returns_stats_grouped_by_date.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/stats/date?end_date=2026-01-31&start_date=2026-01-01 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Mar 2026 10:00:05 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"mno345" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.035000' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"date":"2026-01-01","stats":{"delivery_count":5,"delivery_rate":1.0,"bounce_count":0,"bounce_rate":0.0,"open_count":4,"open_rate":0.8,"click_count":2,"click_rate":0.5,"spam_count":0,"spam_rate":0.0}},{"date":"2026-01-02","stats":{"delivery_count":10,"delivery_rate":0.91,"bounce_count":1,"bounce_rate":0.09,"open_count":8,"open_rate":0.8,"click_count":3,"click_rate":0.38,"spam_count":0,"spam_rate":0.0}}]' + recorded_at: Tue, 04 Mar 2026 10:00:05 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_domain/returns_stats_grouped_by_domain.yml b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_domain/returns_stats_grouped_by_domain.yml new file mode 100644 index 0000000..baf038b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_domain/returns_stats_grouped_by_domain.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/stats/domains?end_date=2026-01-31&start_date=2026-01-01 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Mar 2026 10:00:02 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"def456" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.030000' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"sending_domain_id":1,"stats":{"delivery_count":100,"delivery_rate":0.96,"bounce_count":4,"bounce_rate":0.04,"open_count":80,"open_rate":0.8,"click_count":40,"click_rate":0.5,"spam_count":1,"spam_rate":0.01}},{"sending_domain_id":2,"stats":{"delivery_count":50,"delivery_rate":0.93,"bounce_count":4,"bounce_rate":0.07,"open_count":40,"open_rate":0.8,"click_count":20,"click_rate":0.5,"spam_count":1,"spam_rate":0.02}}]' + recorded_at: Tue, 04 Mar 2026 10:00:02 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_email_service_provider/returns_stats_grouped_by_email_service_provider.yml b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_email_service_provider/returns_stats_grouped_by_email_service_provider.yml new file mode 100644 index 0000000..8e9e4f4 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_by_email_service_provider/returns_stats_grouped_by_email_service_provider.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/stats/email_service_providers?end_date=2026-01-31&start_date=2026-01-01 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Mar 2026 10:00:04 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"jkl012" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.032000' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"email_service_provider":"Gmail","stats":{"delivery_count":80,"delivery_rate":0.97,"bounce_count":2,"bounce_rate":0.03,"open_count":70,"open_rate":0.88,"click_count":35,"click_rate":0.5,"spam_count":1,"spam_rate":0.013}},{"email_service_provider":"Yahoo","stats":{"delivery_count":70,"delivery_rate":0.93,"bounce_count":6,"bounce_rate":0.07,"open_count":50,"open_rate":0.71,"click_count":25,"click_rate":0.5,"spam_count":1,"spam_rate":0.014}}]' + recorded_at: Tue, 04 Mar 2026 10:00:04 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats.yml b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats.yml new file mode 100644 index 0000000..e41a5f9 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/stats?end_date=2026-01-31&start_date=2026-01-01 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Mar 2026 10:00:00 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"abc123" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.025000' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"delivery_count":150,"delivery_rate":0.95,"bounce_count":8,"bounce_rate":0.05,"open_count":120,"open_rate":0.8,"click_count":60,"click_rate":0.5,"spam_count":2,"spam_rate":0.013}' + recorded_at: Tue, 04 Mar 2026 10:00:00 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/when_api_key_is_incorrect/raises_authorization_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/when_api_key_is_incorrect/raises_authorization_error.yml new file mode 100644 index 0000000..aeda103 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/when_api_key_is_incorrect/raises_authorization_error.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/stats?end_date=2026-01-31&start_date=2026-01-01 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Tue, 04 Mar 2026 10:00:01 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '31' + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Www-Authenticate: + - Token realm="Application" + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Cache-Control: + - no-cache + X-Runtime: + - '0.006000' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Incorrect API token"}' + recorded_at: Tue, 04 Mar 2026 10:00:01 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/with_optional_filters/returns_filtered_sending_stats.yml b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/with_optional_filters/returns_filtered_sending_stats.yml new file mode 100644 index 0000000..3322516 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_StatsAPI/_get/with_optional_filters/returns_filtered_sending_stats.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/stats?categories%5B%5D=Transactional&email_service_providers%5B%5D=Gmail&end_date=2026-01-31&sending_domain_ids%5B%5D=1&sending_domain_ids%5B%5D=2&sending_streams%5B%5D=transactional&start_date=2026-01-01 + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Mar 2026 10:00:06 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"pqr678" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.027000' + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"delivery_count":100,"delivery_rate":0.96,"bounce_count":4,"bounce_rate":0.04,"open_count":80,"open_rate":0.8,"click_count":40,"click_rate":0.5,"spam_count":1,"spam_rate":0.01}' + recorded_at: Tue, 04 Mar 2026 10:00:06 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/mailtrap/stats_api_spec.rb b/spec/mailtrap/stats_api_spec.rb new file mode 100644 index 0000000..5400539 --- /dev/null +++ b/spec/mailtrap/stats_api_spec.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::StatsAPI, :vcr do + subject(:stats_api) { described_class.new(account_id, client) } + + let(:account_id) { ENV.fetch('MAILTRAP_ACCOUNT_ID', 1_111_111) } + let(:client) { Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY', 'local-api-key')) } + + let(:start_date) { '2026-01-01' } + let(:end_date) { '2026-01-31' } + + describe '#get' do + subject(:stats) { stats_api.get(start_date: start_date, end_date: end_date) } + + it 'returns aggregated sending stats' do + expect(stats).to be_a(Mailtrap::SendingStats) + + expect(stats).to match_struct( + delivery_count: 150, delivery_rate: 0.95, + bounce_count: 8, bounce_rate: 0.05, + open_count: 120, open_rate: 0.8, + click_count: 60, click_rate: 0.5, + spam_count: 2, spam_rate: 0.013 + ) + end + + context 'with optional filters' do + subject(:stats) do + stats_api.get( + start_date: start_date, + end_date: end_date, + sending_domain_ids: [1, 2], + sending_streams: ['transactional'], + categories: ['Transactional'], + email_service_providers: ['Gmail'] + ) + end + + it 'returns filtered sending stats' do + expect(stats).to be_a(Mailtrap::SendingStats) + + expect(stats).to match_struct( + delivery_count: 100, delivery_rate: 0.96, + bounce_count: 4, bounce_rate: 0.04, + open_count: 80, open_rate: 0.8, + click_count: 40, click_rate: 0.5, + spam_count: 1, spam_rate: 0.01 + ) + end + end + + context 'when api key is incorrect' do + let(:client) { Mailtrap::Client.new(api_key: 'incorrect-api-key') } + + it 'raises authorization error' do + expect { stats }.to raise_error do |error| + expect(error).to be_a(Mailtrap::AuthorizationError) + expect(error.message).to include('Incorrect API token') + expect(error.messages.any? { |msg| msg.include?('Incorrect API token') }).to be true + end + end + end + end + + describe '#by_domain' do + subject(:stats) { stats_api.by_domain(start_date: start_date, end_date: end_date) } + + it 'returns stats grouped by domain' do + expect(stats.size).to eq(2) + expect(stats.first).to match_struct( + name: :sending_domain_id, + value: 1, + stats: match_struct( + delivery_count: 100, delivery_rate: 0.96, + bounce_count: 4, bounce_rate: 0.04, + open_count: 80, open_rate: 0.8, + click_count: 40, click_rate: 0.5, + spam_count: 1, spam_rate: 0.01 + ) + ) + expect(stats.last).to match_struct( + name: :sending_domain_id, + value: 2, + stats: match_struct( + delivery_count: 50, delivery_rate: 0.93, + bounce_count: 4, bounce_rate: 0.07, + open_count: 40, open_rate: 0.8, + click_count: 20, click_rate: 0.5, + spam_count: 1, spam_rate: 0.02 + ) + ) + end + end + + describe '#by_category' do + subject(:stats) { stats_api.by_category(start_date: start_date, end_date: end_date) } + + it 'returns stats grouped by category' do + expect(stats.size).to eq(2) + + expect(stats.first).to match_struct( + name: :category, + value: 'Transactional', + stats: match_struct( + delivery_count: 100, delivery_rate: 0.97, + bounce_count: 3, bounce_rate: 0.03, + open_count: 85, open_rate: 0.85, + click_count: 45, click_rate: 0.53, + spam_count: 0, spam_rate: 0.0 + ) + ) + expect(stats.last).to match_struct( + name: :category, + value: 'Marketing', + stats: match_struct( + delivery_count: 50, delivery_rate: 0.91, + bounce_count: 5, bounce_rate: 0.09, + open_count: 35, open_rate: 0.7, + click_count: 15, click_rate: 0.43, + spam_count: 2, spam_rate: 0.04 + ) + ) + end + end + + describe '#by_email_service_provider' do + subject(:stats) { stats_api.by_email_service_provider(start_date: start_date, end_date: end_date) } + + it 'returns stats grouped by email service provider' do + expect(stats.size).to eq(2) + expect(stats.first).to match_struct( + name: :email_service_provider, + value: 'Gmail', + stats: match_struct( + delivery_count: 80, delivery_rate: 0.97, + bounce_count: 2, bounce_rate: 0.03, + open_count: 70, open_rate: 0.88, + click_count: 35, click_rate: 0.5, + spam_count: 1, spam_rate: 0.013 + ) + ) + expect(stats.last).to match_struct( + name: :email_service_provider, + value: 'Yahoo', + stats: match_struct( + delivery_count: 70, delivery_rate: 0.93, + bounce_count: 6, bounce_rate: 0.07, + open_count: 50, open_rate: 0.71, + click_count: 25, click_rate: 0.5, + spam_count: 1, spam_rate: 0.014 + ) + ) + end + end + + describe '#by_date' do + subject(:stats) { stats_api.by_date(start_date: start_date, end_date: end_date) } + + it 'returns stats grouped by date' do + expect(stats.size).to eq(2) + expect(stats.first).to match_struct( + name: :date, + value: '2026-01-01', + stats: match_struct( + delivery_count: 5, delivery_rate: 1.0, + bounce_count: 0, bounce_rate: 0.0, + open_count: 4, open_rate: 0.8, + click_count: 2, click_rate: 0.5, + spam_count: 0, spam_rate: 0.0 + ) + ) + expect(stats.last).to match_struct( + name: :date, + value: '2026-01-02', + stats: match_struct( + delivery_count: 10, delivery_rate: 0.91, + bounce_count: 1, bounce_rate: 0.09, + open_count: 8, open_rate: 0.8, + click_count: 3, click_rate: 0.38, + spam_count: 0, spam_rate: 0.0 + ) + ) + end + end + + describe 'date validation' do + it 'accepts String dates', vcr: { cassette_name: 'Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats' } do + stats = stats_api.get(start_date: '2026-01-01', end_date: '2026-01-31') + expect(stats).to be_a(Mailtrap::SendingStats) + end + + it 'accepts Date objects', vcr: { cassette_name: 'Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats' } do + stats = stats_api.get(start_date: Date.new(2026, 1, 1), end_date: Date.new(2026, 1, 31)) + expect(stats).to be_a(Mailtrap::SendingStats) + end + + it 'accepts Time objects', vcr: { cassette_name: 'Mailtrap_StatsAPI/_get/returns_aggregated_sending_stats' } do + stats = stats_api.get(start_date: Time.new(2026, 1, 1), end_date: Time.new(2026, 1, 31)) + expect(stats).to be_a(Mailtrap::SendingStats) + end + + it 'raises ArgumentError for invalid start_date' do + expect { stats_api.get(start_date: 'not-a-date', end_date: end_date) } + .to raise_error(ArgumentError, /Invalid date: "not-a-date"/) + end + + it 'raises ArgumentError for invalid end_date' do + expect { stats_api.get(start_date: start_date, end_date: 'bad') } + .to raise_error(ArgumentError, /Invalid date: "bad"/) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ce088d4..ef1be10 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -67,7 +67,13 @@ match do |actual_struct| # Making sure expected keys exist and match expected_ok = expected_attributes.all? do |key, expected_value| - actual_struct.respond_to?(key) && actual_struct[key] == expected_value + next false unless actual_struct.respond_to?(key) + + if expected_value.respond_to?(:matches?) + expected_value.matches?(actual_struct[key]) + else + actual_struct[key] == expected_value + end end # Checking if Struct does not have extra keys that are not in expected_attributes