From c5b5a18540035c8abf9bb14ad056eb770cf0da84 Mon Sep 17 00:00:00 2001 From: sarco3t <14gainward88@gmail.com> Date: Thu, 19 Jun 2025 00:28:20 +0300 Subject: [PATCH 1/6] Suppressions Api --- examples/suppressions_api.rb | 35 ++++ lib/mailtrap.rb | 1 + lib/mailtrap/base_api.rb | 4 +- lib/mailtrap/client.rb | 15 +- lib/mailtrap/suppression.rb | 46 +++++ lib/mailtrap/suppressions_api.rb | 37 ++++ ...s_response_data_to_Suppression_objects.yml | 166 ++++++++++++++++++ .../raises_authorization_error.yml | 166 ++++++++++++++++++ spec/mailtrap/suppression_spec.rb | 45 +++++ spec/mailtrap/suppressions_api_spec.rb | 161 +++++++++++++++++ spec/spec_helper.rb | 15 ++ 11 files changed, 686 insertions(+), 5 deletions(-) create mode 100644 examples/suppressions_api.rb create mode 100644 lib/mailtrap/suppression.rb create mode 100644 lib/mailtrap/suppressions_api.rb create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml create mode 100644 spec/mailtrap/suppression_spec.rb create mode 100644 spec/mailtrap/suppressions_api_spec.rb diff --git a/examples/suppressions_api.rb b/examples/suppressions_api.rb new file mode 100644 index 0000000..47a7bc1 --- /dev/null +++ b/examples/suppressions_api.rb @@ -0,0 +1,35 @@ +require 'mailtrap' + +client = Mailtrap::Client.new(api_key: 'your-api-key') +suppressions = Mailtrap::SuppressionsAPI.new 3229, client + +# Set your API credentials as environment variables +# export MAILTRAP_API_KEY='your-api-key' +# export MAILTRAP_ACCOUNT_ID=your-account-id +# +# suppressions = Mailtrap::SuppressionsAPI.new + +# Get all suppressions +list = suppressions.list +# => +# [ +# Mailtrap::Suppression.new( +# id: "64d71bf3-1276-417b-86e1-8e66f138acfe", +# type: "unsubscription", +# created_at: "2024-12-26T09:40:44.161Z", +# email: "recipient@example.com", +# sending_stream: "transactional", +# domain_name: "sender.com", +# message_bounce_category: nil, +# message_category: "Welcome email", +# message_client_ip: "123.123.123.123", +# message_created_at: "2024-12-26T07:10:00.889Z", +# message_outgoing_ip: "1.1.1.1", +# message_recipient_mx_name: "Other Providers", +# message_sender_email: "hello@sender.com", +# message_subject: "Welcome!" +# ) +# ] + +# Delete a suppression +suppressions.delete(list.first.id) diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index 9d3b856..7c32975 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -9,6 +9,7 @@ require_relative 'mailtrap/contact_lists_api' require_relative 'mailtrap/contact_fields_api' require_relative 'mailtrap/contact_imports_api' +require_relative 'mailtrap/suppressions_api' module Mailtrap # @!macro api_errors diff --git a/lib/mailtrap/base_api.rb b/lib/mailtrap/base_api.rb index 25500b8..6d5b250 100644 --- a/lib/mailtrap/base_api.rb +++ b/lib/mailtrap/base_api.rb @@ -64,8 +64,8 @@ def base_delete(id) client.delete("#{base_path}/#{id}") end - def base_list - response = client.get(base_path) + def base_list(query_params = {}) + response = client.get(base_path, query_params) response.map { |item| handle_response(item) } end diff --git a/lib/mailtrap/client.rb b/lib/mailtrap/client.rb index a3fdbe8..16d02a7 100644 --- a/lib/mailtrap/client.rb +++ b/lib/mailtrap/client.rb @@ -157,10 +157,11 @@ def send(mail) # Performs a GET request to the specified path # @param path [String] The request path + # @param query_params [Hash] Query parameters to append to the URL (optional) # @return [Hash, nil] The JSON response # @!macro api_errors - def get(path) - perform_request(:get, general_api_host, path) + def get(path, query_params = {}) + perform_request(:get, general_api_host, path, nil, query_params) end # Performs a POST request to the specified path @@ -213,13 +214,21 @@ def batch_request_path "/api/batch#{"/#{inbox_id}" if sandbox}" end - def perform_request(method, host, path, body = nil) + def perform_request(method, host, path, body = nil, query_params = {}) http_client = http_client_for(host) + path = build_path_with_query(path, query_params) if query_params.any? + request = setup_request(method, path, body) response = http_client.request(request) handle_response(response) end + def build_path_with_query(base_path, query_params = {}) + uri = URI(base_path) + uri.query = URI.encode_www_form(query_params) + uri.to_s + end + def setup_request(method, path, body = nil) request = case method when :get diff --git a/lib/mailtrap/suppression.rb b/lib/mailtrap/suppression.rb new file mode 100644 index 0000000..3d8308f --- /dev/null +++ b/lib/mailtrap/suppression.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for Suppression + # @see https://api-docs.mailtrap.io/docs/mailtrap-api-docs/f8144826d885a-list-and-search-suppressions + # @attr_reader id [String] The suppression UUID + # @attr_reader type [String] The suppression type + # @attr_reader created_at [String] The creation timestamp + # @attr_reader email [String] The email address + # @attr_reader sending_stream [String] The sending stream + # @attr_reader domain_name [String, nil] The domain name + # @attr_reader message_bounce_category [String, nil] The bounce category + # @attr_reader message_category [String, nil] The message category + # @attr_reader message_client_ip [String, nil] The client IP + # @attr_reader message_created_at [String, nil] The message creation timestamp + # @attr_reader message_esp_response [String, nil] The ESP response + # @attr_reader message_esp_server_type [String, nil] The ESP server type + # @attr_reader message_outgoing_ip [String, nil] The outgoing IP + # @attr_reader message_recipient_mx_name [String, nil] The recipient MX name + # @attr_reader message_sender_email [String, nil] The sender email + # @attr_reader message_subject [String, nil] The message subject + Suppression = Struct.new( + :id, + :type, + :created_at, + :email, + :sending_stream, + :domain_name, + :message_bounce_category, + :message_category, + :message_client_ip, + :message_created_at, + :message_esp_response, + :message_esp_server_type, + :message_outgoing_ip, + :message_recipient_mx_name, + :message_sender_email, + :message_subject, + keyword_init: true + ) do + # @return [Hash] The suppression attributes as a hash + def to_h + super.compact + end + end +end diff --git a/lib/mailtrap/suppressions_api.rb b/lib/mailtrap/suppressions_api.rb new file mode 100644 index 0000000..93f60f8 --- /dev/null +++ b/lib/mailtrap/suppressions_api.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative 'base_api' +require_relative 'suppression' + +module Mailtrap + class SuppressionsAPI + include BaseAPI + + self.response_class = Suppression + + # Lists all suppressions for the account + # @param email [String] Email address to filter suppressions (optional) + # @return [Array] Array of suppression objects + # @!macro api_errors + def list(email: nil) + query_params = {} + query_params[:email] = email if email + + base_list(query_params) + end + + # Deletes a suppression + # @param suppression_id [String] The suppression UUID + # @return nil + # @!macro api_errors + def delete(suppression_id) + client.delete("#{base_path}/#{suppression_id}") + end + + private + + def base_path + "/api/accounts/#{account_id}/suppressions" + end + end +end diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml new file mode 100644 index 0000000..3aec68b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/maps_response_data_to_Suppression_objects.yml @@ -0,0 +1,166 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/suppressions + 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/railsware/mailtrap-ruby) + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 27 Jun 2025 07:56:23 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/"f549768628f00ba29406ba914e08f1e4" + Cache-Control: + - max-age=0, private, must-revalidate + Content-Security-Policy: + - 'default-src ''self''; style-src ''self'' data: blob: ''unsafe-inline'' assets.mailtrap.io + www.googletagmanager.com fonts.googleapis.com; font-src ''self'' data: blob: + ''unsafe-inline'' assets.mailtrap.io static.hsappstatic.net fonts.gstatic.cn + fonts.gstatic.com *.s-microsoft.com use.typekit.net; script-src ''self'' data: + blob: assets.mailtrap.io *.cookiebot.com www.clarity.ms *.doubleclick.net + *.googlesyndication.com *.googletagmanager.com www.googleadservices.com www.google.com + beacon-v2.helpscout.net js.hs-analytics.net js.hs-banner.com js.hs-scripts.com + cdn.firstpromoter.com connect.facebook.net www.recaptcha.net www.gstatic.cn + www.gstatic.com *.quora.com static.ads-twitter.com snap.licdn.com *.growthbook.io + translate.google.com ''nonce-js-LnrTELrR9S0wzLm2PNRFKw==''; connect-src ''self'' + assets.mailtrap.io wss://mailtrap.io *.google.com *.google.ad *.google.ae + *.google.com.af *.google.com.ag *.google.al *.google.am *.google.co.ao *.google.com.ar + *.google.as *.google.at *.google.com.au *.google.az *.google.ba *.google.com.bd + *.google.be *.google.bf *.google.bg *.google.com.bh *.google.bi *.google.bj + *.google.com.bn *.google.com.bo *.google.com.br *.google.bs *.google.bt *.google.co.bw + *.google.by *.google.com.bz *.google.ca *.google.cd *.google.cf *.google.cg + *.google.ch *.google.ci *.google.co.ck *.google.cl *.google.cm *.google.cn + *.google.com.co *.google.co.cr *.google.com.cu *.google.cv *.google.com.cy + *.google.cz *.google.de *.google.dj *.google.dk *.google.dm *.google.com.do + *.google.dz *.google.com.ec *.google.ee *.google.com.eg *.google.es *.google.com.et + *.google.fi *.google.com.fj *.google.fm *.google.fr *.google.ga *.google.ge + *.google.gg *.google.com.gh *.google.com.gi *.google.gl *.google.gm *.google.gr + *.google.com.gt *.google.gy *.google.com.hk *.google.hn *.google.hr *.google.ht + *.google.hu *.google.co.id *.google.ie *.google.co.il *.google.im *.google.co.in + *.google.iq *.google.is *.google.it *.google.je *.google.com.jm *.google.jo + *.google.co.jp *.google.co.ke *.google.com.kh *.google.ki *.google.kg *.google.co.kr + *.google.com.kw *.google.kz *.google.la *.google.com.lb *.google.li *.google.lk + *.google.co.ls *.google.lt *.google.lu *.google.lv *.google.com.ly *.google.co.ma + *.google.md *.google.me *.google.mg *.google.mk *.google.ml *.google.com.mm + *.google.mn *.google.com.mt *.google.mu *.google.mv *.google.mw *.google.com.mx + *.google.com.my *.google.co.mz *.google.com.na *.google.com.ng *.google.com.ni + *.google.ne *.google.nl *.google.no *.google.com.np *.google.nr *.google.nu + *.google.co.nz *.google.com.om *.google.com.pa *.google.com.pe *.google.com.pg + *.google.com.ph *.google.com.pk *.google.pl *.google.pn *.google.com.pr *.google.ps + *.google.pt *.google.com.py *.google.com.qa *.google.ro *.google.ru *.google.rw + *.google.com.sa *.google.com.sb *.google.sc *.google.se *.google.com.sg *.google.sh + *.google.si *.google.sk *.google.com.sl *.google.sn *.google.so *.google.sm + *.google.sr *.google.st *.google.com.sv *.google.td *.google.tg *.google.co.th + *.google.com.tj *.google.tl *.google.tm *.google.tn *.google.to *.google.com.tr + *.google.tt *.google.com.tw *.google.co.tz *.google.com.ua *.google.co.ug + *.google.co.uk *.google.com.uy *.google.co.uz *.google.com.vc *.google.co.ve + *.google.co.vi *.google.com.vn *.google.vu *.google.ws *.google.rs *.google.co.za + *.google.co.zm *.google.co.zw *.google.cat errors.rw.rw *.cookiebot.com *.clarity.ms + *.g.doubleclick.net *.googlesyndication.com *.googletagmanager.com www.google.com + wss://ws-helpscout.pusher.com sockjs-helpscout.pusher.com *.helpscout.net + *.firstpromoter.com connect.facebook.net *.facebook.com www.recaptcha.net + *.analytics.google.com *.google-analytics.com *.quora.com *.linkedin.com analytics.twitter.com + t.co/1/i/adsct *.growthbook.io meta-gateway.mailtrap.io translate-pa.googleapis.com; + img-src ''self'' data: blob: assets.mailtrap.io *.google.com *.google.ad *.google.ae + *.google.com.af *.google.com.ag *.google.al *.google.am *.google.co.ao *.google.com.ar + *.google.as *.google.at *.google.com.au *.google.az *.google.ba *.google.com.bd + *.google.be *.google.bf *.google.bg *.google.com.bh *.google.bi *.google.bj + *.google.com.bn *.google.com.bo *.google.com.br *.google.bs *.google.bt *.google.co.bw + *.google.by *.google.com.bz *.google.ca *.google.cd *.google.cf *.google.cg + *.google.ch *.google.ci *.google.co.ck *.google.cl *.google.cm *.google.cn + *.google.com.co *.google.co.cr *.google.com.cu *.google.cv *.google.com.cy + *.google.cz *.google.de *.google.dj *.google.dk *.google.dm *.google.com.do + *.google.dz *.google.com.ec *.google.ee *.google.com.eg *.google.es *.google.com.et + *.google.fi *.google.com.fj *.google.fm *.google.fr *.google.ga *.google.ge + *.google.gg *.google.com.gh *.google.com.gi *.google.gl *.google.gm *.google.gr + *.google.com.gt *.google.gy *.google.com.hk *.google.hn *.google.hr *.google.ht + *.google.hu *.google.co.id *.google.ie *.google.co.il *.google.im *.google.co.in + *.google.iq *.google.is *.google.it *.google.je *.google.com.jm *.google.jo + *.google.co.jp *.google.co.ke *.google.com.kh *.google.ki *.google.kg *.google.co.kr + *.google.com.kw *.google.kz *.google.la *.google.com.lb *.google.li *.google.lk + *.google.co.ls *.google.lt *.google.lu *.google.lv *.google.com.ly *.google.co.ma + *.google.md *.google.me *.google.mg *.google.mk *.google.ml *.google.com.mm + *.google.mn *.google.com.mt *.google.mu *.google.mv *.google.mw *.google.com.mx + *.google.com.my *.google.co.mz *.google.com.na *.google.com.ng *.google.com.ni + *.google.ne *.google.nl *.google.no *.google.com.np *.google.nr *.google.nu + *.google.co.nz *.google.com.om *.google.com.pa *.google.com.pe *.google.com.pg + *.google.com.ph *.google.com.pk *.google.pl *.google.pn *.google.com.pr *.google.ps + *.google.pt *.google.com.py *.google.com.qa *.google.ro *.google.ru *.google.rw + *.google.com.sa *.google.com.sb *.google.sc *.google.se *.google.com.sg *.google.sh + *.google.si *.google.sk *.google.com.sl *.google.sn *.google.so *.google.sm + *.google.sr *.google.st *.google.com.sv *.google.td *.google.tg *.google.co.th + *.google.com.tj *.google.tl *.google.tm *.google.tn *.google.to *.google.com.tr + *.google.tt *.google.com.tw *.google.co.tz *.google.com.ua *.google.co.ug + *.google.co.uk *.google.com.uy *.google.co.uz *.google.com.vc *.google.co.ve + *.google.co.vi *.google.com.vn *.google.vu *.google.ws *.google.rs *.google.co.za + *.google.co.zm *.google.co.zw *.google.cat *.cookiebot.com *.clarity.ms *.doubleclick.net + *.googlesyndication.com *.googletagmanager.com *.google.com track.hubspot.com + *.facebook.com *.facebook.net *.analytics.google.com *.google-analytics.com + *.quora.com *.linkedin.com analytics.twitter.com t.co/1/i/adsct secure.gravatar.com; + frame-src ''self'' consentcdn.cookiebot.com td.doubleclick.net www.googletagmanager.com + www.facebook.com www.recaptcha.net translate.googleapis.com; frame-ancestors + ''self''; media-src ''self'' data: blob: beacon-v2.helpscout.net ssl.gstatic.com; + object-src ''self'' beacon-v2.helpscout.net; report-uri https://errors.rw.rw/api/37/security/?sentry_key=5a0cc8a2cb4f49a8b9043c602e4ec0ab' + X-Request-Id: + - cfd58e1b-f272-4859-97bd-42f942568d0e + X-Runtime: + - '0.065844' + X-Cloud-Trace-Context: + - c592cc41ea204c3e89e485f1ff924281;o=0 + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 956363349bf90230-WAW + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"id":"af4485b5-5181-4fb5-9d41-287c78f708c1","type":"manual import","created_at":"2025-06-27T07:56:17Z","email":"asd@asd.com","sending_stream":"transactional","domain_name":"","message_bounce_category":"","message_category":"","message_client_ip":"","message_created_at":"","message_esp_response":"","message_esp_server_type":"","message_outgoing_ip":"","message_recipient_mx_name":"","message_sender_email":"","message_subject":""}]' + recorded_at: Fri, 27 Jun 2025 07:56:23 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml new file mode 100644 index 0000000..255a419 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SuppressionsAPI/vcr_list/when_api_key_is_incorrect/raises_authorization_error.yml @@ -0,0 +1,166 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/suppressions + 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/railsware/mailtrap-ruby) + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Fri, 27 Jun 2025 07:56:23 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 + Content-Security-Policy: + - 'default-src ''self''; style-src ''self'' data: blob: ''unsafe-inline'' assets.mailtrap.io + www.googletagmanager.com fonts.googleapis.com; font-src ''self'' data: blob: + ''unsafe-inline'' assets.mailtrap.io static.hsappstatic.net fonts.gstatic.cn + fonts.gstatic.com *.s-microsoft.com use.typekit.net; script-src ''self'' data: + blob: assets.mailtrap.io *.cookiebot.com www.clarity.ms *.doubleclick.net + *.googlesyndication.com *.googletagmanager.com www.googleadservices.com www.google.com + beacon-v2.helpscout.net js.hs-analytics.net js.hs-banner.com js.hs-scripts.com + cdn.firstpromoter.com connect.facebook.net www.recaptcha.net www.gstatic.cn + www.gstatic.com *.quora.com static.ads-twitter.com snap.licdn.com *.growthbook.io + translate.google.com ''nonce-js-0jfLtIMAiuTVrlyqJ7/btA==''; connect-src ''self'' + assets.mailtrap.io wss://mailtrap.io *.google.com *.google.ad *.google.ae + *.google.com.af *.google.com.ag *.google.al *.google.am *.google.co.ao *.google.com.ar + *.google.as *.google.at *.google.com.au *.google.az *.google.ba *.google.com.bd + *.google.be *.google.bf *.google.bg *.google.com.bh *.google.bi *.google.bj + *.google.com.bn *.google.com.bo *.google.com.br *.google.bs *.google.bt *.google.co.bw + *.google.by *.google.com.bz *.google.ca *.google.cd *.google.cf *.google.cg + *.google.ch *.google.ci *.google.co.ck *.google.cl *.google.cm *.google.cn + *.google.com.co *.google.co.cr *.google.com.cu *.google.cv *.google.com.cy + *.google.cz *.google.de *.google.dj *.google.dk *.google.dm *.google.com.do + *.google.dz *.google.com.ec *.google.ee *.google.com.eg *.google.es *.google.com.et + *.google.fi *.google.com.fj *.google.fm *.google.fr *.google.ga *.google.ge + *.google.gg *.google.com.gh *.google.com.gi *.google.gl *.google.gm *.google.gr + *.google.com.gt *.google.gy *.google.com.hk *.google.hn *.google.hr *.google.ht + *.google.hu *.google.co.id *.google.ie *.google.co.il *.google.im *.google.co.in + *.google.iq *.google.is *.google.it *.google.je *.google.com.jm *.google.jo + *.google.co.jp *.google.co.ke *.google.com.kh *.google.ki *.google.kg *.google.co.kr + *.google.com.kw *.google.kz *.google.la *.google.com.lb *.google.li *.google.lk + *.google.co.ls *.google.lt *.google.lu *.google.lv *.google.com.ly *.google.co.ma + *.google.md *.google.me *.google.mg *.google.mk *.google.ml *.google.com.mm + *.google.mn *.google.com.mt *.google.mu *.google.mv *.google.mw *.google.com.mx + *.google.com.my *.google.co.mz *.google.com.na *.google.com.ng *.google.com.ni + *.google.ne *.google.nl *.google.no *.google.com.np *.google.nr *.google.nu + *.google.co.nz *.google.com.om *.google.com.pa *.google.com.pe *.google.com.pg + *.google.com.ph *.google.com.pk *.google.pl *.google.pn *.google.com.pr *.google.ps + *.google.pt *.google.com.py *.google.com.qa *.google.ro *.google.ru *.google.rw + *.google.com.sa *.google.com.sb *.google.sc *.google.se *.google.com.sg *.google.sh + *.google.si *.google.sk *.google.com.sl *.google.sn *.google.so *.google.sm + *.google.sr *.google.st *.google.com.sv *.google.td *.google.tg *.google.co.th + *.google.com.tj *.google.tl *.google.tm *.google.tn *.google.to *.google.com.tr + *.google.tt *.google.com.tw *.google.co.tz *.google.com.ua *.google.co.ug + *.google.co.uk *.google.com.uy *.google.co.uz *.google.com.vc *.google.co.ve + *.google.co.vi *.google.com.vn *.google.vu *.google.ws *.google.rs *.google.co.za + *.google.co.zm *.google.co.zw *.google.cat errors.rw.rw *.cookiebot.com *.clarity.ms + *.g.doubleclick.net *.googlesyndication.com *.googletagmanager.com www.google.com + wss://ws-helpscout.pusher.com sockjs-helpscout.pusher.com *.helpscout.net + *.firstpromoter.com connect.facebook.net *.facebook.com www.recaptcha.net + *.analytics.google.com *.google-analytics.com *.quora.com *.linkedin.com analytics.twitter.com + t.co/1/i/adsct *.growthbook.io meta-gateway.mailtrap.io translate-pa.googleapis.com; + img-src ''self'' data: blob: assets.mailtrap.io *.google.com *.google.ad *.google.ae + *.google.com.af *.google.com.ag *.google.al *.google.am *.google.co.ao *.google.com.ar + *.google.as *.google.at *.google.com.au *.google.az *.google.ba *.google.com.bd + *.google.be *.google.bf *.google.bg *.google.com.bh *.google.bi *.google.bj + *.google.com.bn *.google.com.bo *.google.com.br *.google.bs *.google.bt *.google.co.bw + *.google.by *.google.com.bz *.google.ca *.google.cd *.google.cf *.google.cg + *.google.ch *.google.ci *.google.co.ck *.google.cl *.google.cm *.google.cn + *.google.com.co *.google.co.cr *.google.com.cu *.google.cv *.google.com.cy + *.google.cz *.google.de *.google.dj *.google.dk *.google.dm *.google.com.do + *.google.dz *.google.com.ec *.google.ee *.google.com.eg *.google.es *.google.com.et + *.google.fi *.google.com.fj *.google.fm *.google.fr *.google.ga *.google.ge + *.google.gg *.google.com.gh *.google.com.gi *.google.gl *.google.gm *.google.gr + *.google.com.gt *.google.gy *.google.com.hk *.google.hn *.google.hr *.google.ht + *.google.hu *.google.co.id *.google.ie *.google.co.il *.google.im *.google.co.in + *.google.iq *.google.is *.google.it *.google.je *.google.com.jm *.google.jo + *.google.co.jp *.google.co.ke *.google.com.kh *.google.ki *.google.kg *.google.co.kr + *.google.com.kw *.google.kz *.google.la *.google.com.lb *.google.li *.google.lk + *.google.co.ls *.google.lt *.google.lu *.google.lv *.google.com.ly *.google.co.ma + *.google.md *.google.me *.google.mg *.google.mk *.google.ml *.google.com.mm + *.google.mn *.google.com.mt *.google.mu *.google.mv *.google.mw *.google.com.mx + *.google.com.my *.google.co.mz *.google.com.na *.google.com.ng *.google.com.ni + *.google.ne *.google.nl *.google.no *.google.com.np *.google.nr *.google.nu + *.google.co.nz *.google.com.om *.google.com.pa *.google.com.pe *.google.com.pg + *.google.com.ph *.google.com.pk *.google.pl *.google.pn *.google.com.pr *.google.ps + *.google.pt *.google.com.py *.google.com.qa *.google.ro *.google.ru *.google.rw + *.google.com.sa *.google.com.sb *.google.sc *.google.se *.google.com.sg *.google.sh + *.google.si *.google.sk *.google.com.sl *.google.sn *.google.so *.google.sm + *.google.sr *.google.st *.google.com.sv *.google.td *.google.tg *.google.co.th + *.google.com.tj *.google.tl *.google.tm *.google.tn *.google.to *.google.com.tr + *.google.tt *.google.com.tw *.google.co.tz *.google.com.ua *.google.co.ug + *.google.co.uk *.google.com.uy *.google.co.uz *.google.com.vc *.google.co.ve + *.google.co.vi *.google.com.vn *.google.vu *.google.ws *.google.rs *.google.co.za + *.google.co.zm *.google.co.zw *.google.cat *.cookiebot.com *.clarity.ms *.doubleclick.net + *.googlesyndication.com *.googletagmanager.com *.google.com track.hubspot.com + *.facebook.com *.facebook.net *.analytics.google.com *.google-analytics.com + *.quora.com *.linkedin.com analytics.twitter.com t.co/1/i/adsct secure.gravatar.com; + frame-src ''self'' consentcdn.cookiebot.com td.doubleclick.net www.googletagmanager.com + www.facebook.com www.recaptcha.net translate.googleapis.com; frame-ancestors + ''self''; media-src ''self'' data: blob: beacon-v2.helpscout.net ssl.gstatic.com; + object-src ''self'' beacon-v2.helpscout.net; report-uri https://errors.rw.rw/api/37/security/?sentry_key=5a0cc8a2cb4f49a8b9043c602e4ec0ab' + X-Request-Id: + - 378759fd-0ab0-468c-be7e-b1b4c4d2f972 + X-Runtime: + - '0.009793' + X-Cloud-Trace-Context: + - 51d1a8094ff74f07cf18a0ed4e656380;o=0 + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 956363363a4ebf92-WAW + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Incorrect API token"}' + recorded_at: Fri, 27 Jun 2025 07:56:23 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/mailtrap/suppression_spec.rb b/spec/mailtrap/suppression_spec.rb new file mode 100644 index 0000000..9e8662f --- /dev/null +++ b/spec/mailtrap/suppression_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::Suppression do + let(:attributes) do + { + id: '018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132', + type: 'bounce', + created_at: '2021-01-01T00:00:00Z', + email: 'test@example.com', + sending_stream: 'main', + domain_name: 'example.com', + message_bounce_category: 'hard_bounce', + message_category: 'bounce', + message_client_ip: '192.168.1.1', + message_created_at: '2021-01-01T00:00:00Z', + message_esp_response: '550 5.1.1 User unknown', + message_esp_server_type: 'smtp', + message_outgoing_ip: '10.0.0.1', + message_recipient_mx_name: 'mx.example.com', + message_sender_email: 'sender@example.com', + message_subject: 'Test Email' + } + end + + describe '#initialize' do + subject(:suppression) { described_class.new(attributes) } + + it 'creates a suppression with all attributes' do + expect(suppression).to have_attributes(attributes) + end + end + + describe '#to_h' do + subject(:hash) { suppression.to_h } + + let(:suppression) do + described_class.new(attributes) + end + + it 'returns a hash with all attributes' do + expect(hash).to have_different_object_id_than(attributes) + expect(hash).to eq(attributes) + end + end +end diff --git a/spec/mailtrap/suppressions_api_spec.rb b/spec/mailtrap/suppressions_api_spec.rb new file mode 100644 index 0000000..fe2c261 --- /dev/null +++ b/spec/mailtrap/suppressions_api_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::SuppressionsAPI do + subject(:suppressions) { 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(:base_url) { "https://mailtrap.io/api/accounts/#{account_id}" } + + describe '#list' do + let(:expected_attributes) do + { + 'id' => '123e4567-e89b-12d3-a456-426614174000', + 'type' => 'hard bounce', + 'created_at' => '2024-06-01T12:00:00Z', + 'email' => 'user1@example.com', + 'sending_stream' => 'transactional', + 'domain_name' => 'example.com', + 'message_bounce_category' => 'invalid recipient', + 'message_category' => 'transactional', + 'message_client_ip' => '192.0.2.1', + 'message_created_at' => '2024-06-01T11:59:00Z', + 'message_esp_response' => '550 5.1.1 User unknown', + 'message_esp_server_type' => 'smtp', + 'message_outgoing_ip' => '198.51.100.1', + 'message_recipient_mx_name' => 'mx.example.com', + 'message_sender_email' => 'sender@example.com', + 'message_subject' => 'Test subject' + } + end + let(:expected_response) do + [ + expected_attributes, + { + 'id' => '456e7890-e89b-12d3-a456-426614174001', + 'type' => 'spam complaint', + 'created_at' => '2024-06-01T13:00:00Z', + 'email' => 'user2@example.com', + 'sending_stream' => 'bulk', + 'domain_name' => 'example.org', + 'message_bounce_category' => nil, + 'message_category' => 'bulk', + 'message_client_ip' => '192.0.2.2', + 'message_created_at' => '2024-06-01T12:59:00Z', + 'message_esp_response' => nil, + 'message_esp_server_type' => nil, + 'message_outgoing_ip' => '198.51.100.2', + 'message_recipient_mx_name' => 'mx.example.org', + 'message_sender_email' => 'sender2@example.com', + 'message_subject' => 'Bulk email subject' + } + ] + end + + it 'returns all suppressions' do + stub_request(:get, "#{base_url}/suppressions") + .to_return( + status: 200, + body: expected_response.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + response = suppressions.list + expect(response).to all(be_a(Mailtrap::Suppression)) + expect(response.length).to eq(2) + expect(response.first).to have_attributes(expected_attributes) + end + + it 'returns suppressions filtered by email' do + email = 'user1@example.com' + stub_request(:get, "#{base_url}/suppressions?email=#{email}") + .to_return( + status: 200, + body: [expected_attributes].to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + response = suppressions.list(email:) + expect(response).to all(be_a(Mailtrap::Suppression)) + expect(response.length).to eq(1) + expect(response.first).to have_attributes(expected_attributes) + end + + it 'raises error when unauthorized' do + stub_request(:get, "#{base_url}/suppressions") + .to_return( + status: 401, + body: { 'error' => 'Unauthorized' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { suppressions.list }.to raise_error(Mailtrap::AuthorizationError) + end + end + + describe '#delete' do + let(:suppression_id) { 1 } + + it 'deletes a suppression' do + stub_request(:delete, "#{base_url}/suppressions/#{suppression_id}") + .to_return(status: 204) + + response = suppressions.delete(suppression_id) + expect(response).to be_nil + end + + it 'raises error when suppression not found' do + stub_request(:delete, "#{base_url}/suppressions/999") + .to_return( + status: 404, + body: { 'error' => 'Not Found' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { suppressions.delete(999) }.to raise_error(Mailtrap::Error) + end + + it 'raises error when unauthorized' do + stub_request(:delete, "#{base_url}/suppressions/#{suppression_id}") + .to_return( + status: 401, + body: { 'error' => 'Unauthorized' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + expect { suppressions.delete(suppression_id) }.to raise_error(Mailtrap::AuthorizationError) + end + end + + describe 'vcr#list', :vcr do + subject(:list) { suppressions.list } + + it 'maps response data to Suppression objects' do + expect(list).to all(be_a(Mailtrap::Suppression)) + expect(list.first).to have_attributes( + id: be_a(String), + type: be_a(String), + created_at: be_a(String), + email: be_a(String), + sending_stream: be_a(String), + domain_name: be_a(String), + message_bounce_category: be_a(String), + message_category: be_a(String), + message_client_ip: be_a(String) + ) + end + + context 'when api key is incorrect' do + let(:client) { Mailtrap::Client.new(api_key: 'incorrect-api-key') } + + it 'raises authorization error' do + expect { list }.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 +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 86b5e11..35c9290 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -47,3 +47,18 @@ c.syntax = :expect end end + +# Custom matcher to verify object IDs are different +RSpec::Matchers.define :have_different_object_id_than do |expected| + match do |actual| + !actual.equal?(expected) + end + + failure_message do |actual| + "expected #{actual.inspect} (object_id: #{actual.object_id}) to have a different object_id than #{expected.inspect} (object_id: #{expected.object_id})" # rubocop:disable Layout/LineLength + end + + failure_message_when_negated do |actual| + "expected #{actual.inspect} (object_id: #{actual.object_id}) to have the same object_id as #{expected.inspect} (object_id: #{expected.object_id})" # rubocop:disable Layout/LineLength + end +end From b2a0f2bb59ed9c9539ef0a87c8852c4477bfea8f Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Wed, 24 Sep 2025 17:55:31 +0200 Subject: [PATCH 2/6] Fix suppressions example --- examples/suppressions_api.rb | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/examples/suppressions_api.rb b/examples/suppressions_api.rb index 47a7bc1..172f0b6 100644 --- a/examples/suppressions_api.rb +++ b/examples/suppressions_api.rb @@ -13,22 +13,23 @@ list = suppressions.list # => # [ -# Mailtrap::Suppression.new( -# id: "64d71bf3-1276-417b-86e1-8e66f138acfe", -# type: "unsubscription", -# created_at: "2024-12-26T09:40:44.161Z", -# email: "recipient@example.com", -# sending_stream: "transactional", -# domain_name: "sender.com", -# message_bounce_category: nil, -# message_category: "Welcome email", -# message_client_ip: "123.123.123.123", -# message_created_at: "2024-12-26T07:10:00.889Z", -# message_outgoing_ip: "1.1.1.1", -# message_recipient_mx_name: "Other Providers", -# message_sender_email: "hello@sender.com", -# message_subject: "Welcome!" -# ) +# # # ] # Delete a suppression From 2aaa54c2aeb3a1597b8fcddddc11219258575499 Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Wed, 24 Sep 2025 17:58:05 +0200 Subject: [PATCH 3/6] Add an example for listing suppressions for email --- examples/suppressions_api.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/suppressions_api.rb b/examples/suppressions_api.rb index 172f0b6..1ba9c77 100644 --- a/examples/suppressions_api.rb +++ b/examples/suppressions_api.rb @@ -10,7 +10,7 @@ # suppressions = Mailtrap::SuppressionsAPI.new # Get all suppressions -list = suppressions.list +suppressions.list # => # [ # # # ] +# Get suppressions for the email +list = suppressions.list(email: 'recipient@example.com') + # Delete a suppression suppressions.delete(list.first.id) From 7110c1d9aa185027305f6c6a0ff30cb0552be22e Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Wed, 24 Sep 2025 18:40:31 +0200 Subject: [PATCH 4/6] Build uri with query --- lib/mailtrap/client.rb | 71 ++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/lib/mailtrap/client.rb b/lib/mailtrap/client.rb index 16d02a7..fd40e2f 100644 --- a/lib/mailtrap/client.rb +++ b/lib/mailtrap/client.rb @@ -119,10 +119,15 @@ def initialize( # rubocop:disable Metrics/ParameterLists # @!macro api_errors # @raise [Mailtrap::MailSizeError] If the message is too large. def send_batch(base, requests) - perform_request(:post, api_host, batch_request_path, { - base:, - requests: - }) + perform_request( + method: :post, + host: api_host, + path: batch_request_path, + body: { + base:, + requests: + } + ) end # Sends an email @@ -152,7 +157,12 @@ def send_batch(base, requests) # @!macro api_errors # @raise [Mailtrap::MailSizeError] If the message is too large def send(mail) - perform_request(:post, api_host, send_path, mail) + perform_request( + method: :post, + host: api_host, + path: send_path, + body: mail + ) end # Performs a GET request to the specified path @@ -161,7 +171,12 @@ def send(mail) # @return [Hash, nil] The JSON response # @!macro api_errors def get(path, query_params = {}) - perform_request(:get, general_api_host, path, nil, query_params) + perform_request( + method: :get, + host: general_api_host, + path:, + query_params: + ) end # Performs a POST request to the specified path @@ -170,7 +185,12 @@ def get(path, query_params = {}) # @return [Hash, nil] The JSON response # @!macro api_errors def post(path, body = nil) - perform_request(:post, general_api_host, path, body) + perform_request( + method: :post, + host: general_api_host, + path:, + body: + ) end # Performs a PATCH request to the specified path @@ -179,7 +199,12 @@ def post(path, body = nil) # @return [Hash, nil] The JSON response # @!macro api_errors def patch(path, body = nil) - perform_request(:patch, general_api_host, path, body) + perform_request( + method: :patch, + host: general_api_host, + path:, + body: + ) end # Performs a DELETE request to the specified path @@ -187,7 +212,11 @@ def patch(path, body = nil) # @return [Hash, nil] The JSON response # @!macro api_errors def delete(path) - perform_request(:delete, general_api_host, path) + perform_request( + method: :delete, + host: general_api_host, + path: + ) end private @@ -214,31 +243,27 @@ def batch_request_path "/api/batch#{"/#{inbox_id}" if sandbox}" end - def perform_request(method, host, path, body = nil, query_params = {}) + def perform_request(method:, host:, path:, query_params: {}, body: nil) http_client = http_client_for(host) - path = build_path_with_query(path, query_params) if query_params.any? - request = setup_request(method, path, body) + uri = URI::HTTPS.build(host:, path:) + uri.query = URI.encode_www_form(query_params) if query_params.any? + + request = setup_request(method, uri, body) response = http_client.request(request) handle_response(response) end - def build_path_with_query(base_path, query_params = {}) - uri = URI(base_path) - uri.query = URI.encode_www_form(query_params) - uri.to_s - end - - def setup_request(method, path, body = nil) + def setup_request(method, uri_or_path, body = nil) request = case method when :get - Net::HTTP::Get.new(path) + Net::HTTP::Get.new(uri_or_path) when :post - Net::HTTP::Post.new(path) + Net::HTTP::Post.new(uri_or_path) when :patch - Net::HTTP::Patch.new(path) + Net::HTTP::Patch.new(uri_or_path) when :delete - Net::HTTP::Delete.new(path) + Net::HTTP::Delete.new(uri_or_path) else raise ArgumentError, "Unsupported HTTP method: #{method}" end From f5a944af46d5d33da00f3b82e147835fe823f153 Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Thu, 25 Sep 2025 10:09:32 +0200 Subject: [PATCH 5/6] Simplify suppression spec --- spec/mailtrap/suppression_spec.rb | 5 +---- spec/spec_helper.rb | 15 --------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/spec/mailtrap/suppression_spec.rb b/spec/mailtrap/suppression_spec.rb index 9e8662f..f9d78d2 100644 --- a/spec/mailtrap/suppression_spec.rb +++ b/spec/mailtrap/suppression_spec.rb @@ -31,15 +31,12 @@ end describe '#to_h' do - subject(:hash) { suppression.to_h } - let(:suppression) do described_class.new(attributes) end it 'returns a hash with all attributes' do - expect(hash).to have_different_object_id_than(attributes) - expect(hash).to eq(attributes) + expect(suppression.to_h).to eq(attributes) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 35c9290..86b5e11 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -47,18 +47,3 @@ c.syntax = :expect end end - -# Custom matcher to verify object IDs are different -RSpec::Matchers.define :have_different_object_id_than do |expected| - match do |actual| - !actual.equal?(expected) - end - - failure_message do |actual| - "expected #{actual.inspect} (object_id: #{actual.object_id}) to have a different object_id than #{expected.inspect} (object_id: #{expected.object_id})" # rubocop:disable Layout/LineLength - end - - failure_message_when_negated do |actual| - "expected #{actual.inspect} (object_id: #{actual.object_id}) to have the same object_id as #{expected.inspect} (object_id: #{expected.object_id})" # rubocop:disable Layout/LineLength - end -end From d2e34e22ad5c675929d0fcbe7decbbb111fa9a7b Mon Sep 17 00:00:00 2001 From: Ivan Yurchanka Date: Thu, 25 Sep 2025 10:18:16 +0200 Subject: [PATCH 6/6] Add account_id variable in examples --- examples/contacts_api.rb | 9 +++++---- examples/email_templates_api.rb | 3 ++- examples/suppressions_api.rb | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/contacts_api.rb b/examples/contacts_api.rb index 598cb11..b50f496 100644 --- a/examples/contacts_api.rb +++ b/examples/contacts_api.rb @@ -1,10 +1,11 @@ require 'mailtrap' +account_id = 3229 client = Mailtrap::Client.new(api_key: 'your-api-key') -contact_lists = Mailtrap::ContactListsAPI.new 3229, client -contacts = Mailtrap::ContactsAPI.new 3229, client -contact_fields = Mailtrap::ContactFieldsAPI.new 3229, client -contact_imports = Mailtrap::ContactImportsAPI.new 3229, client +contact_lists = Mailtrap::ContactListsAPI.new(account_id, client) +contacts = Mailtrap::ContactsAPI.new(account_id, client) +contact_fields = Mailtrap::ContactFieldsAPI.new(account_id, client) +contact_imports = Mailtrap::ContactImportsAPI.new(account_id, client) # Set your API credentials as environment variables # export MAILTRAP_API_KEY='your-api-key' diff --git a/examples/email_templates_api.rb b/examples/email_templates_api.rb index f614e71..cb4c222 100644 --- a/examples/email_templates_api.rb +++ b/examples/email_templates_api.rb @@ -1,7 +1,8 @@ require 'mailtrap' +account_id = 3229 client = Mailtrap::Client.new(api_key: 'your-api-key') -templates = Mailtrap::EmailTemplatesAPI.new 3229, client +templates = Mailtrap::EmailTemplatesAPI.new(account_id, client) # Set your API credentials as environment variables # export MAILTRAP_API_KEY='your-api-key' diff --git a/examples/suppressions_api.rb b/examples/suppressions_api.rb index 1ba9c77..a09dffb 100644 --- a/examples/suppressions_api.rb +++ b/examples/suppressions_api.rb @@ -1,7 +1,8 @@ require 'mailtrap' +account_id = 3229 client = Mailtrap::Client.new(api_key: 'your-api-key') -suppressions = Mailtrap::SuppressionsAPI.new 3229, client +suppressions = Mailtrap::SuppressionsAPI.new(account_id, client) # Set your API credentials as environment variables # export MAILTRAP_API_KEY='your-api-key'