diff --git a/examples/suppressions_api.rb b/examples/suppressions_api.rb new file mode 100644 index 0000000..97a8bbc --- /dev/null +++ b/examples/suppressions_api.rb @@ -0,0 +1,16 @@ +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 + +# Delete a suppression +suppressions.delete(list.first.id) diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index 9858086..c6cce24 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -4,7 +4,9 @@ require_relative 'mailtrap/mail' require_relative 'mailtrap/errors' require_relative 'mailtrap/version' +require_relative 'mailtrap/base_api' require_relative 'mailtrap/email_templates_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 new file mode 100644 index 0000000..ae61e32 --- /dev/null +++ b/lib/mailtrap/base_api.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mailtrap + class BaseAPI + attr_reader :account_id, :client + + # @param account_id [Integer] The account ID + # @param client [Mailtrap::Client] The client instance + # @raise [ArgumentError] If account_id is nil + def initialize(account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Mailtrap::Client.new) + raise ArgumentError, 'account_id is required' if account_id.nil? + + @account_id = account_id + @client = client + end + + private + + def validate_options!(options, supported_options) + invalid_options = options.keys - supported_options + return if invalid_options.empty? + + raise ArgumentError, "invalid options are given: #{invalid_options}, supported_options: #{supported_options}" + end + + def build_entity(options, response_class) + response_class.new(options.slice(*response_class.members)) + end + end +end diff --git a/lib/mailtrap/client.rb b/lib/mailtrap/client.rb index 8b9d9e3..1cb7337 100644 --- a/lib/mailtrap/client.rb +++ b/lib/mailtrap/client.rb @@ -67,10 +67,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 @@ -121,9 +122,10 @@ def send_path "/api/send#{sandbox ? "/#{inbox_id}" : ""}" 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) request = setup_request(method, path, body) + request.query = URI.encode_www_form(query_params) if query_params.any? response = http_client.request(request) handle_response(response) end diff --git a/lib/mailtrap/email_templates_api.rb b/lib/mailtrap/email_templates_api.rb index a929839..b064dbf 100644 --- a/lib/mailtrap/email_templates_api.rb +++ b/lib/mailtrap/email_templates_api.rb @@ -3,28 +3,16 @@ require_relative 'email_template' module Mailtrap - class EmailTemplatesAPI + class EmailTemplatesAPI < BaseAPI SUPPORTED_OPTIONS = %i[name subject category body_html body_text].freeze private_constant :SUPPORTED_OPTIONS - attr_reader :account_id, :client - - # @param account_id [Integer] The account ID - # @param client [Mailtrap::Client] The client instance - # @raise [ArgumentError] If account_id is nil - def initialize(account_id = ENV.fetch('MAILTRAP_ACCOUNT_ID'), client = Client.new) - raise ArgumentError, 'account_id is required' if account_id.nil? - - @account_id = account_id - @client = client - end - # Lists all email templates for the account # @return [Array] Array of template objects # @!macro api_errors def list response = client.get(base_path) - response.map { |template| build_email_template(template) } + response.map { |template| build_entity(template, EmailTemplate) } end # Retrieves a specific email template @@ -33,7 +21,7 @@ def list # @!macro api_errors def get(template_id) response = client.get("#{base_path}/#{template_id}") - build_email_template(response) + build_entity(response, EmailTemplate) end # Creates a new email template @@ -47,10 +35,10 @@ def get(template_id) # @!macro api_errors # @raise [ArgumentError] If invalid options are provided def create(options) - validate_options!(options) + validate_options!(options, SUPPORTED_OPTIONS) response = client.post(base_path, email_template: options) - build_email_template(response) + build_entity(response, EmailTemplate) end # Updates an existing email template @@ -65,10 +53,10 @@ def create(options) # @!macro api_errors # @raise [ArgumentError] If invalid options are provided def update(template_id, options) - validate_options!(options) + validate_options!(options, SUPPORTED_OPTIONS) response = client.patch("#{base_path}/#{template_id}", email_template: options) - build_email_template(response) + build_entity(response, EmailTemplate) end # Deletes an email template @@ -81,19 +69,8 @@ def delete(template_id) private - def build_email_template(options) - EmailTemplate.new(options.slice(*EmailTemplate.members)) - end - def base_path "/api/accounts/#{account_id}/email_templates" end - - def validate_options!(options) - invalid_options = options.keys - SUPPORTED_OPTIONS - return if invalid_options.empty? - - raise ArgumentError, "invalid options are given: #{invalid_options}, supported_options: #{SUPPORTED_OPTIONS}" - end end end 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..95d81bd --- /dev/null +++ b/lib/mailtrap/suppressions_api.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative 'suppression' + +module Mailtrap + class SuppressionsAPI < BaseAPI + # 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 + + response = client.get(base_path, query_params) + response.map { |suppression| build_entity(suppression, Suppression) } + 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..1116bbd --- /dev/null +++ b/spec/mailtrap/suppressions_api_spec.rb @@ -0,0 +1,146 @@ +# 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 '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..362f443 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.object_id != expected.object_id + 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