From 78ff1b799c86e27e184e2d37690ee08697127623 Mon Sep 17 00:00:00 2001 From: Marcin Klocek Date: Mon, 5 Oct 2026 16:20:15 +0200 Subject: [PATCH] Add inbound forward rules, forward outcomes, thread delivery and search BREAKING CHANGE: InboundThreadMessage no longer has delivery_status, delivered_at and bounced_at; read them from delivery instead. --- README.md | 1 + examples/inbound_forward_rules_api.rb | 34 ++++++ examples/inbound_messages_api.rb | 3 + examples/inbound_threads_api.rb | 19 +++- lib/mailtrap.rb | 1 + lib/mailtrap/inbound_forward_outcome.rb | 21 ++++ lib/mailtrap/inbound_forward_rule.rb | 21 ++++ .../inbound_forward_rule_condition.rb | 17 +++ .../inbound_forward_rule_destination.rb | 11 ++ lib/mailtrap/inbound_forward_rules_api.rb | 106 ++++++++++++++++++ lib/mailtrap/inbound_message.rb | 2 + lib/mailtrap/inbound_messages_api.rb | 8 ++ lib/mailtrap/inbound_thread_message.rb | 13 +-- .../inbound_thread_message_delivery.rb | 17 +++ lib/mailtrap/inbound_threads_api.rb | 19 +++- ...e_data_to_an_InboundForwardRule_object.yml | 74 ++++++++++++ .../_delete/returns_nil.yml | 65 +++++++++++ .../raises_an_error.yml | 71 ++++++++++++ ...e_data_to_an_InboundForwardRule_object.yml | 74 ++++++++++++ .../raises_an_error.yml | 71 ++++++++++++ ...nse_data_to_InboundForwardRule_objects.yml | 75 +++++++++++++ ...e_data_to_an_InboundForwardRule_object.yml | 74 ++++++++++++ .../returns_the_rule_without_conditions.yml | 74 ++++++++++++ ...wards_to_InboundForwardOutcome_objects.yml | 78 +++++++++++++ ...ages_and_forwards_on_received_messages.yml | 92 +++++++++++++++ ...earch_and_returns_the_matching_threads.yml | 77 +++++++++++++ spec/mailtrap/inbound_forward_outcome_spec.rb | 27 +++++ .../inbound_forward_rule_condition_spec.rb | 18 +++ .../inbound_forward_rule_destination_spec.rb | 11 ++ spec/mailtrap/inbound_forward_rule_spec.rb | 34 ++++++ .../inbound_forward_rules_api_spec.rb | 105 +++++++++++++++++ spec/mailtrap/inbound_message_spec.rb | 6 +- spec/mailtrap/inbound_messages_api_spec.rb | 9 ++ .../inbound_thread_message_delivery_spec.rb | 23 ++++ spec/mailtrap/inbound_thread_message_spec.rb | 12 +- spec/mailtrap/inbound_threads_api_spec.rb | 23 ++++ spec/spec_helper.rb | 5 + 37 files changed, 1374 insertions(+), 17 deletions(-) create mode 100644 examples/inbound_forward_rules_api.rb create mode 100644 lib/mailtrap/inbound_forward_outcome.rb create mode 100644 lib/mailtrap/inbound_forward_rule.rb create mode 100644 lib/mailtrap/inbound_forward_rule_condition.rb create mode 100644 lib/mailtrap/inbound_forward_rule_destination.rb create mode 100644 lib/mailtrap/inbound_forward_rules_api.rb create mode 100644 lib/mailtrap/inbound_thread_message_delivery.rb create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_create/maps_response_data_to_an_InboundForwardRule_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/returns_nil.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/when_the_forward_rule_does_not_exist/raises_an_error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/maps_response_data_to_an_InboundForwardRule_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/when_the_forward_rule_does_not_exist/raises_an_error.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_list/maps_response_data_to_InboundForwardRule_objects.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/maps_response_data_to_an_InboundForwardRule_object.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/when_clearing_conditions/returns_the_rule_without_conditions.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_get/with_forwards/maps_forwards_to_InboundForwardOutcome_objects.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_get/with_a_forwarded_message_and_a_sent_reply/maps_delivery_on_sent_messages_and_forwards_on_received_messages.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_list/with_search/passes_search_and_returns_the_matching_threads.yml create mode 100644 spec/mailtrap/inbound_forward_outcome_spec.rb create mode 100644 spec/mailtrap/inbound_forward_rule_condition_spec.rb create mode 100644 spec/mailtrap/inbound_forward_rule_destination_spec.rb create mode 100644 spec/mailtrap/inbound_forward_rule_spec.rb create mode 100644 spec/mailtrap/inbound_forward_rules_api_spec.rb create mode 100644 spec/mailtrap/inbound_thread_message_delivery_spec.rb diff --git a/README.md b/README.md index d94bbd0d..9b8e83b3 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ Inbound Email: - Inbound Inboxes API – [`inbound_inboxes_api.rb`](examples/inbound_inboxes_api.rb) - Inbound Messages API (list, get, delete, reply, reply_all, forward) – [`inbound_messages_api.rb`](examples/inbound_messages_api.rb) - Inbound Threads API – [`inbound_threads_api.rb`](examples/inbound_threads_api.rb) +- Inbound Forward Rules API – [`inbound_forward_rules_api.rb`](examples/inbound_forward_rules_api.rb) Contact management: diff --git a/examples/inbound_forward_rules_api.rb b/examples/inbound_forward_rules_api.rb new file mode 100644 index 00000000..cabdad8f --- /dev/null +++ b/examples/inbound_forward_rules_api.rb @@ -0,0 +1,34 @@ +require 'mailtrap' + +client = Mailtrap::Client.new(api_key: 'your-api-key') +forward_rules = Mailtrap::InboundForwardRulesAPI.new(client) +inbox_id = 42 + +# Create a forward rule +rule = forward_rules.create( + inbox_id, + name: 'Copy billing mail to finance', + conditions: [{ match_type: 'sender', operator: 'ends_with', value: '@billing.example.com' }], + destinations: [{ email: 'finance@example.com' }] +) +# => # + +# List forward rules +forward_rules.list(inbox_id) +# => [#] + +# Get a forward rule +forward_rules.get(inbox_id, rule.id) +# => # + +# Update a forward rule +forward_rules.update( + inbox_id, + rule.id, + destinations: [{ email: 'finance@example.com' }, { email: 'accounting@example.com' }] +) +# => # + +# Delete a forward rule +forward_rules.delete(inbox_id, rule.id) +# => nil diff --git a/examples/inbound_messages_api.rb b/examples/inbound_messages_api.rb index e4992e30..c8006c33 100644 --- a/examples/inbound_messages_api.rb +++ b/examples/inbound_messages_api.rb @@ -15,6 +15,9 @@ message = messages.get(inbox_id, page.data.first.id) # => # +message.forwards +# => [#] + # Reply to the original sender messages.reply(inbox_id, message.id, text: 'Thanks for reaching out.') # => # diff --git a/examples/inbound_threads_api.rb b/examples/inbound_threads_api.rb index 765f7370..cffecedf 100644 --- a/examples/inbound_threads_api.rb +++ b/examples/inbound_threads_api.rb @@ -11,10 +11,27 @@ # Fetch the next page with the returned cursor threads.list(inbox_id, last_id: page.last_id) if page.last_id +results = threads.list(inbox_id, search: 'acme') +# => # + +threads.list(inbox_id, search: 'acme', last_id: results.last_id) if results.last_id + # Get a single thread with its messages embedded (oldest first) -threads.get(inbox_id, page.data.first.id) +thread = threads.get(inbox_id, page.data.first.id) # => # +thread.messages.each do |message| + next unless message.visibility_status == 'available' + + if message.direction == 'outbound' + message.delivery + # => # + else + message.forwards + # => [#] + end +end + # Delete a thread threads.delete(inbox_id, page.data.first.id) # => nil diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index 44dca60c..4a07b39e 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -34,6 +34,7 @@ require_relative 'mailtrap/inbound_inboxes_api' require_relative 'mailtrap/inbound_messages_api' require_relative 'mailtrap/inbound_threads_api' +require_relative 'mailtrap/inbound_forward_rules_api' module Mailtrap # @!macro api_errors diff --git a/lib/mailtrap/inbound_forward_outcome.rb b/lib/mailtrap/inbound_forward_outcome.rb new file mode 100644 index 00000000..c2a373fd --- /dev/null +++ b/lib/mailtrap/inbound_forward_outcome.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for an inbound forward outcome + # @see https://docs.mailtrap.io/developers/inbound + # @attr_reader rule_id [Integer] The forward rule ID + # @attr_reader rule_name [String, nil] The forward rule name + # @attr_reader destination [String] The destination address + # @attr_reader status [String] forwarded or rejected + # @attr_reader reason [String, nil] The rejection reason + # @attr_reader message_id [String, nil] The forwarded message ID + InboundForwardOutcome = Struct.new( + :rule_id, + :rule_name, + :destination, + :status, + :reason, + :message_id, + keyword_init: true + ) +end diff --git a/lib/mailtrap/inbound_forward_rule.rb b/lib/mailtrap/inbound_forward_rule.rb new file mode 100644 index 00000000..dd748327 --- /dev/null +++ b/lib/mailtrap/inbound_forward_rule.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for an inbound forward rule + # @see https://docs.mailtrap.io/developers/inbound + # @attr_reader id [Integer] The forward rule ID + # @attr_reader name [String] The rule name + # @attr_reader created_at [String] Creation timestamp + # @attr_reader updated_at [String] Last update timestamp + # @attr_reader conditions [Array] The rule conditions + # @attr_reader destinations [Array] The rule destinations + InboundForwardRule = Struct.new( + :id, + :name, + :created_at, + :updated_at, + :conditions, + :destinations, + keyword_init: true + ) +end diff --git a/lib/mailtrap/inbound_forward_rule_condition.rb b/lib/mailtrap/inbound_forward_rule_condition.rb new file mode 100644 index 00000000..c40fa551 --- /dev/null +++ b/lib/mailtrap/inbound_forward_rule_condition.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for an inbound forward rule condition + # @see https://docs.mailtrap.io/developers/inbound + # @attr_reader match_type [String] The match type + # @attr_reader operator [String] The comparison operator + # @attr_reader value [String, nil] The value to compare against + # @attr_reader header_key [String, nil] The header name + InboundForwardRuleCondition = Struct.new( + :match_type, + :operator, + :value, + :header_key, + keyword_init: true + ) +end diff --git a/lib/mailtrap/inbound_forward_rule_destination.rb b/lib/mailtrap/inbound_forward_rule_destination.rb new file mode 100644 index 00000000..a79e6e30 --- /dev/null +++ b/lib/mailtrap/inbound_forward_rule_destination.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for an inbound forward rule destination + # @see https://docs.mailtrap.io/developers/inbound + # @attr_reader email [String] The destination email address + InboundForwardRuleDestination = Struct.new( + :email, + keyword_init: true + ) +end diff --git a/lib/mailtrap/inbound_forward_rules_api.rb b/lib/mailtrap/inbound_forward_rules_api.rb new file mode 100644 index 00000000..80b4cd12 --- /dev/null +++ b/lib/mailtrap/inbound_forward_rules_api.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require_relative 'base_api' +require_relative 'inbound_forward_rule' +require_relative 'inbound_forward_rule_condition' +require_relative 'inbound_forward_rule_destination' + +module Mailtrap + class InboundForwardRulesAPI + include BaseAPI + + self.supported_options = %i[name conditions destinations] + self.response_class = InboundForwardRule + + # @param client [Mailtrap::Client] The client instance + def initialize(client = Mailtrap::Client.new) + @client = client + end + + # Lists forward rules in an inbox + # @param inbox_id [Integer] The inbox ID + # @return [Array] Array of forward rules + # @!macro api_errors + def list(inbox_id) + response = client.get(forward_rules_path(inbox_id)) + Array(response[:data]).map { |item| build_rule(item) } + end + + # Retrieves a specific forward rule + # @param inbox_id [Integer] The inbox ID + # @param forward_rule_id [Integer] The forward rule ID + # @return [InboundForwardRule] Forward rule object + # @!macro api_errors + def get(inbox_id, forward_rule_id) + handle_response(client.get("#{forward_rules_path(inbox_id)}/#{forward_rule_id}")) + end + + # Creates a new forward rule + # @param inbox_id [Integer] The inbox ID + # @param [Hash] options The parameters to create + # @option options [String] :name The rule name + # @option options [Array] :conditions The rule conditions + # @option options [Array] :destinations The rule destinations + # @return [InboundForwardRule] Created forward rule + # @!macro api_errors + # @raise [ArgumentError] If invalid options are provided + def create(inbox_id, options) + validate_options!(options, supported_options) + handle_response(client.post(forward_rules_path(inbox_id), options)) + end + + # Updates a forward rule + # @param inbox_id [Integer] The inbox ID + # @param forward_rule_id [Integer] The forward rule ID + # @param [Hash] options The parameters to update + # @option options [String] :name The rule name + # @option options [Array] :conditions The rule conditions + # @option options [Array] :destinations The rule destinations + # @return [InboundForwardRule] Updated forward rule + # @!macro api_errors + # @raise [ArgumentError] If invalid options are provided + def update(inbox_id, forward_rule_id, options) + validate_options!(options, supported_options) + handle_response(client.patch("#{forward_rules_path(inbox_id)}/#{forward_rule_id}", options)) + end + + # Deletes a forward rule + # @param inbox_id [Integer] The inbox ID + # @param forward_rule_id [Integer] The forward rule ID + # @return nil + # @!macro api_errors + def delete(inbox_id, forward_rule_id) + client.delete("#{forward_rules_path(inbox_id)}/#{forward_rule_id}") + end + + private + + def forward_rules_path(inbox_id) + "/api/inbound/inboxes/#{inbox_id}/forward_rules" + end + + def handle_response(response) + build_rule(response[:data]) + end + + def build_rule(hash) + attrs = hash.slice(*InboundForwardRule.members) + attrs[:conditions] = build_conditions(attrs[:conditions]) if attrs[:conditions] + attrs[:destinations] = build_destinations(attrs[:destinations]) if attrs[:destinations] + + InboundForwardRule.new(**attrs) + end + + def build_conditions(conditions) + Array(conditions).map do |condition| + InboundForwardRuleCondition.new(**condition.slice(*InboundForwardRuleCondition.members)) + end + end + + def build_destinations(destinations) + Array(destinations).map do |destination| + InboundForwardRuleDestination.new(**destination.slice(*InboundForwardRuleDestination.members)) + end + end + end +end diff --git a/lib/mailtrap/inbound_message.rb b/lib/mailtrap/inbound_message.rb index 1da64a27..fc77d6df 100644 --- a/lib/mailtrap/inbound_message.rb +++ b/lib/mailtrap/inbound_message.rb @@ -25,6 +25,7 @@ module Mailtrap # @attr_reader raw_message_expires_at [String, nil] When the raw message URL expires (only when fetched by ID) # @attr_reader html_body [String, nil] Decoded HTML body (only when fetched by ID) # @attr_reader text_body [String, nil] Decoded text body (only when fetched by ID) + # @attr_reader forwards [Array] The forward outcomes # rubocop:disable Lint/StructNewOverride -- +size+ is an API field that shadows Struct#size InboundMessage = Struct.new( :id, @@ -49,6 +50,7 @@ module Mailtrap :raw_message_expires_at, :html_body, :text_body, + :forwards, keyword_init: true ) # rubocop:enable Lint/StructNewOverride diff --git a/lib/mailtrap/inbound_messages_api.rb b/lib/mailtrap/inbound_messages_api.rb index 7fb93283..265041e3 100644 --- a/lib/mailtrap/inbound_messages_api.rb +++ b/lib/mailtrap/inbound_messages_api.rb @@ -3,6 +3,7 @@ require_relative 'base_api' require_relative 'inbound_message' require_relative 'inbound_attachment' +require_relative 'inbound_forward_outcome' require_relative 'inbound_messages_list_response' require_relative 'inbound_send_result' @@ -100,6 +101,7 @@ def send_action(inbox_id, message_id, action, options) def build_message(hash) attrs = hash.slice(*InboundMessage.members) attrs[:attachments] = build_attachments(attrs[:attachments]) if attrs[:attachments] + attrs[:forwards] = build_forwards(attrs[:forwards]) if attrs[:forwards] InboundMessage.new(**attrs) end @@ -109,5 +111,11 @@ def build_attachments(attachments) InboundAttachment.new(**attachment.slice(*InboundAttachment.members)) end end + + def build_forwards(forwards) + Array(forwards).map do |forward| + InboundForwardOutcome.new(**forward.slice(*InboundForwardOutcome.members)) + end + end end end diff --git a/lib/mailtrap/inbound_thread_message.rb b/lib/mailtrap/inbound_thread_message.rb index fe7a7028..f86e974e 100644 --- a/lib/mailtrap/inbound_thread_message.rb +++ b/lib/mailtrap/inbound_thread_message.rb @@ -2,8 +2,7 @@ module Mailtrap # Data Transfer Object for a message inside an inbound thread. - # Only +visibility_status+ and +direction+ are always present; +placeholder+ entries omit the rest, - # and the delivery lifecycle fields are only set on available outbound messages. + # Only +visibility_status+ and +direction+ are always present; +placeholder+ entries omit the rest. # @see https://docs.mailtrap.io/developers/inbound # @attr_reader visibility_status [String] available or placeholder # @attr_reader direction [String] inbound or outbound @@ -23,9 +22,8 @@ module Mailtrap # @attr_reader text_body [String, nil] Decoded text body # @attr_reader html_body [String, nil] Decoded HTML body # @attr_reader attachments [Array, nil] The message attachments - # @attr_reader delivery_status [String, nil] Delivery status (outbound messages only) - # @attr_reader delivered_at [String, nil] Delivery timestamp (outbound messages only) - # @attr_reader bounced_at [String, nil] Bounce timestamp (outbound messages only) + # @attr_reader delivery [InboundThreadMessageDelivery, nil] Delivery outcome (outbound messages only) + # @attr_reader forwards [Array, nil] Forward outcomes (inbound messages only) InboundThreadMessage = Struct.new( :visibility_status, :direction, @@ -45,9 +43,8 @@ module Mailtrap :text_body, :html_body, :attachments, - :delivery_status, - :delivered_at, - :bounced_at, + :delivery, + :forwards, keyword_init: true ) end diff --git a/lib/mailtrap/inbound_thread_message_delivery.rb b/lib/mailtrap/inbound_thread_message_delivery.rb new file mode 100644 index 00000000..8d5e95f6 --- /dev/null +++ b/lib/mailtrap/inbound_thread_message_delivery.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for the delivery of an outbound message inside an inbound thread + # @see https://docs.mailtrap.io/developers/inbound + # @attr_reader to [String] The recipient address + # @attr_reader status [String] The delivery status + # @attr_reader delivered_at [String, nil] Delivery timestamp + # @attr_reader bounced_at [String, nil] Bounce timestamp + InboundThreadMessageDelivery = Struct.new( + :to, + :status, + :delivered_at, + :bounced_at, + keyword_init: true + ) +end diff --git a/lib/mailtrap/inbound_threads_api.rb b/lib/mailtrap/inbound_threads_api.rb index 25aa25eb..ae3219fe 100644 --- a/lib/mailtrap/inbound_threads_api.rb +++ b/lib/mailtrap/inbound_threads_api.rb @@ -4,6 +4,8 @@ require_relative 'inbound_thread' require_relative 'inbound_thread_message' require_relative 'inbound_attachment' +require_relative 'inbound_forward_outcome' +require_relative 'inbound_thread_message_delivery' require_relative 'inbound_threads_list_response' module Mailtrap @@ -20,10 +22,11 @@ def initialize(client = Mailtrap::Client.new) # Lists threads in an inbox (cursor-paginated) # @param inbox_id [Integer] The inbox ID # @param last_id [String, nil] Cursor from the previous response's +last_id+ for the next page + # @param search [String, nil] Search query # @return [InboundThreadsListResponse] data, total_count, and last_id # @!macro api_errors - def list(inbox_id, last_id: nil) - query_params = last_id ? { last_id: } : {} + def list(inbox_id, last_id: nil, search: nil) + query_params = { last_id:, search: }.compact response = client.get(threads_path(inbox_id), query_params) InboundThreadsListResponse.new( @@ -69,6 +72,8 @@ def build_messages(messages) Array(messages).map do |message| attrs = message.slice(*InboundThreadMessage.members) attrs[:attachments] = build_attachments(attrs[:attachments]) if attrs[:attachments] + attrs[:delivery] = build_delivery(attrs[:delivery]) if attrs[:delivery] + attrs[:forwards] = build_forwards(attrs[:forwards]) if attrs[:forwards] InboundThreadMessage.new(**attrs) end @@ -79,5 +84,15 @@ def build_attachments(attachments) InboundAttachment.new(**attachment.slice(*InboundAttachment.members)) end end + + def build_delivery(delivery) + InboundThreadMessageDelivery.new(**delivery.slice(*InboundThreadMessageDelivery.members)) + end + + def build_forwards(forwards) + Array(forwards).map do |forward| + InboundForwardOutcome.new(**forward.slice(*InboundForwardOutcome.members)) + end + end end end diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_create/maps_response_data_to_an_InboundForwardRule_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_create/maps_response_data_to_an_InboundForwardRule_object.yml new file mode 100644 index 00000000..767e86bf --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_create/maps_response_data_to_an_InboundForwardRule_object.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: post + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules + body: + encoding: UTF-8 + string: '{"name":"Copy billing mail to finance","conditions":[{"match_type":"sender","operator":"ends_with","value":"@billing.example.com"}],"destinations":[{"email":"finance@example.com"}]}' + 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: 201 + message: Created + headers: + Date: + - Mon, 05 Oct 2026 14:14:04 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '295' + 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/"cb7e9b84695cc86d37240a44e313ae55" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.141589' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"data":{"id":2,"name":"Copy billing mail to finance","created_at":"2026-10-05T14:14:03.975Z","updated_at":"2026-10-05T14:14:03.975Z","conditions":[{"match_type":"sender","operator":"ends_with","value":"@billing.example.com","header_key":null}],"destinations":[{"email": + "welcome@rw.com"}]}}' + recorded_at: Mon, 05 Oct 2026 14:14:04 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/returns_nil.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/returns_nil.yml new file mode 100644 index 00000000..f9bdf3c9 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/returns_nil.yml @@ -0,0 +1,65 @@ +--- +http_interactions: +- request: + method: delete + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules/2 + 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: 204 + message: No Content + headers: + Date: + - Mon, 05 Oct 2026 14:17:50 GMT + 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 + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Cache-Control: + - no-cache + X-Runtime: + - '0.060817' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '' + recorded_at: Mon, 05 Oct 2026 14:17:50 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/when_the_forward_rule_does_not_exist/raises_an_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/when_the_forward_rule_does_not_exist/raises_an_error.yml new file mode 100644 index 00000000..118f457e --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_delete/when_the_forward_rule_does_not_exist/raises_an_error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: delete + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules/-1 + 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: 404 + message: Not Found + headers: + Date: + - Mon, 05 Oct 2026 14:17:50 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '21' + 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: + - '148' + Cache-Control: + - no-cache + X-Runtime: + - '0.024156' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Not Found"}' + recorded_at: Mon, 05 Oct 2026 14:17:50 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/maps_response_data_to_an_InboundForwardRule_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/maps_response_data_to_an_InboundForwardRule_object.yml new file mode 100644 index 00000000..b68afd03 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/maps_response_data_to_an_InboundForwardRule_object.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules/2 + 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: + - Mon, 05 Oct 2026 14:15:33 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: + - '148' + Etag: + - W/"cb7e9b84695cc86d37240a44e313ae55" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.063985' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":{"id":2,"name":"Copy billing mail to finance","created_at":"2026-10-05T14:14:03.975Z","updated_at":"2026-10-05T14:14:03.975Z","conditions":[{"match_type":"sender","operator":"ends_with","value":"@billing.example.com","header_key":null}],"destinations":[{"email": + "welcome@rw.com"}]}}' + recorded_at: Mon, 05 Oct 2026 14:15:33 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/when_the_forward_rule_does_not_exist/raises_an_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/when_the_forward_rule_does_not_exist/raises_an_error.yml new file mode 100644 index 00000000..2ebc0e77 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_get/when_the_forward_rule_does_not_exist/raises_an_error.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules/-1 + 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: 404 + message: Not Found + headers: + Date: + - Mon, 05 Oct 2026 14:15:33 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '21' + 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: + - '147' + Cache-Control: + - no-cache + X-Runtime: + - '0.020555' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Not Found"}' + recorded_at: Mon, 05 Oct 2026 14:15:33 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_list/maps_response_data_to_InboundForwardRule_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_list/maps_response_data_to_InboundForwardRule_objects.yml new file mode 100644 index 00000000..69ce1cac --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_list/maps_response_data_to_InboundForwardRule_objects.yml @@ -0,0 +1,75 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules + 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: + - Mon, 05 Oct 2026 14:15:33 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/"c6c27cba6f0e4bce60887df70a173add" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.058044' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":[{"id":1,"name":"SDK VCR copy everything","created_at":"2026-10-02T14:56:33.933Z","updated_at":"2026-10-02T14:56:33.933Z","conditions":[],"destinations":[{"email": + "welcome@rw.com"}]},{"id":2,"name":"Copy billing mail to finance","created_at":"2026-10-05T14:14:03.975Z","updated_at":"2026-10-05T14:14:03.975Z","conditions":[{"match_type":"sender","operator":"ends_with","value":"@billing.example.com","header_key":null}],"destinations":[{"email": + "welcome@rw.com"}]}]}' + recorded_at: Mon, 05 Oct 2026 14:15:33 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/maps_response_data_to_an_InboundForwardRule_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/maps_response_data_to_an_InboundForwardRule_object.yml new file mode 100644 index 00000000..62b9a887 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/maps_response_data_to_an_InboundForwardRule_object.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: patch + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules/2 + body: + encoding: UTF-8 + string: '{"destinations":[{"email":"finance@example.com"},{"email":"accounting@example.com"}]}' + 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: + - Mon, 05 Oct 2026 14:15:34 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: + - '146' + Etag: + - W/"df0dddb11faee86ab8b95822dc90b2c4" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.136950' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":{"id":2,"name":"Copy billing mail to finance","created_at":"2026-10-05T14:14:03.975Z","updated_at":"2026-10-05T14:15:34.081Z","conditions":[{"match_type":"sender","operator":"ends_with","value":"@billing.example.com","header_key":null}],"destinations":[{"email": + "welcome@rw.com"},{"email": "welcome@rw.com"}]}}' + recorded_at: Mon, 05 Oct 2026 14:15:34 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/when_clearing_conditions/returns_the_rule_without_conditions.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/when_clearing_conditions/returns_the_rule_without_conditions.yml new file mode 100644 index 00000000..4880ec50 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundForwardRulesAPI/_update/when_clearing_conditions/returns_the_rule_without_conditions.yml @@ -0,0 +1,74 @@ +--- +http_interactions: +- request: + method: patch + uri: https://mailtrap.io/api/inbound/inboxes/3924/forward_rules/2 + body: + encoding: UTF-8 + string: '{"conditions":[]}' + 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: + - Mon, 05 Oct 2026 14:15:34 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: + - '145' + Etag: + - W/"4b16457f0dfc2293ac34f47336b9fd3c" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.116970' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":{"id":2,"name":"Copy billing mail to finance","created_at":"2026-10-05T14:14:03.975Z","updated_at":"2026-10-05T14:15:34.386Z","conditions":[],"destinations":[{"email": + "welcome@rw.com"},{"email": "welcome@rw.com"}]}}' + recorded_at: Mon, 05 Oct 2026 14:15:34 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_get/with_forwards/maps_forwards_to_InboundForwardOutcome_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_get/with_forwards/maps_forwards_to_InboundForwardOutcome_objects.yml new file mode 100644 index 00000000..0e40af54 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundMessagesAPI/_get/with_forwards/maps_forwards_to_InboundForwardOutcome_objects.yml @@ -0,0 +1,78 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/inbound/inboxes/3924/messages/1878219290673225792 + 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: + - Mon, 05 Oct 2026 14:16:05 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + Vary: + - Accept + - Accept-Encoding + 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 + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '147' + Etag: + - W/"4c59bfb4b64b6ae6ae85756d8c0247be" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.126158' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"1878219290673225792","inbox_id":3924,"from":"SDK VCR customer + ","to":["support@sdk-smoke-tests.mailtrap.link"],"cc":[],"bcc":[],"reply_to":null,"subject":"Acme + order 20261005141136","headers":{"mime-version":"1.0","return-path":""},"size":2592,"html_size":null,"text_size":42,"received_at":"2026-10-05T14:11:38.092Z","rfc_message_id":"","in_reply_to":null,"references":[],"thread_id":"1878219290603651072","attachments":[],"forwards":[{"rule_id":1,"rule_name":"SDK + VCR copy everything","destination":"welcome@rw.com","status":"forwarded","reason":null,"message_id":"aeb4fb30-c0c6-11f1-0040-f1fba91def36"}],"raw_message_url":"https://example.com/signed-url","raw_message_expires_at":"2026-10-05T15:16:05.033Z","html_body":null,"text_body":"Hello, + I have a question about my order.\r\n"}' + recorded_at: Mon, 05 Oct 2026 14:16:05 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_get/with_a_forwarded_message_and_a_sent_reply/maps_delivery_on_sent_messages_and_forwards_on_received_messages.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_get/with_a_forwarded_message_and_a_sent_reply/maps_delivery_on_sent_messages_and_forwards_on_received_messages.yml new file mode 100644 index 00000000..e9a16f03 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_get/with_a_forwarded_message_and_a_sent_reply/maps_delivery_on_sent_messages_and_forwards_on_received_messages.yml @@ -0,0 +1,92 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/inbound/inboxes/3924/threads/1878219290603651072 + 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: + - Mon, 05 Oct 2026 14:16:04 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + Vary: + - Accept + - Accept-Encoding + 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 + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '148' + Etag: + - W/"0694dfb9ffbff7444f0e38eaf0e68fb9" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.120062' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"1878219290603651072","subject":"Acme order 20261005141136","message_count":5,"size":9134,"first_message_at":"2026-10-05T14:11:38.019Z","last_received_at":"2026-10-05T14:11:49.776Z","last_sent_at":"2026-10-05T14:11:50.519Z","last_activity_at":"2026-10-05T14:11:50.519Z","last_message_id":"1878219303702581248","senders":["SDK + VCR customer ","support@sdk-smoke-tests.mailtrap.link","sdk-vcr-customer@sdk-smoke-tests.mailtrap.link"],"recipients":["support@sdk-smoke-tests.mailtrap.link","SDK + VCR customer ","forwarded@example.com"],"attachments":[],"messages":[{"visibility_status":"available","direction":"outbound","id":"adfb6f30-c0c6-11f1-0000-f19ecc788a53","message_group_id":null,"subject":"Acme + order 20261005141136","rfc_message_id":"","in_reply_to":null,"references":[],"from":"SDK + VCR customer ","to":["support@sdk-smoke-tests.mailtrap.link"],"cc":[],"bcc":[],"reply_to":"","created_at":"2026-10-05T14:11:38.019Z","email_size":340,"text_body":"Hello, + I have a question about my order.","html_body":null,"attachments":[],"delivery_status":"delivered","delivered_at":"2026-10-05T14:11:38.108Z","bounced_at":null,"delivery":{"to":"support@sdk-smoke-tests.mailtrap.link","status":"delivered","delivered_at":"2026-10-05T14:11:38.108Z","bounced_at":null}},{"visibility_status":"available","direction":"inbound","id":"1878219290673225792","message_group_id":null,"subject":"Acme + order 20261005141136","rfc_message_id":"","in_reply_to":null,"references":[],"from":"SDK + VCR customer ","to":["support@sdk-smoke-tests.mailtrap.link"],"cc":[],"bcc":[],"reply_to":"","created_at":"2026-10-05T14:11:38.092Z","email_size":2592,"text_body":"Hello, + I have a question about my order.\r\n","html_body":null,"attachments":[],"forwards":[{"rule_id":1,"rule_name":"SDK + VCR copy everything","destination":"welcome@rw.com","status":"forwarded","reason":null,"message_id":"aeb4fb30-c0c6-11f1-0040-f1fba91def36"}]},{"visibility_status":"available","direction":"outbound","id":"b4f5a0d0-c0c6-11f1-0000-f19ecc788a53","message_group_id":null,"subject":"Re: + Acme order 20261005141136","rfc_message_id":"","in_reply_to":"","references":[""],"from":"support@sdk-smoke-tests.mailtrap.link","to":["SDK + VCR customer "],"cc":[],"bcc":[],"reply_to":"","created_at":"2026-10-05T14:11:49.724Z","email_size":490,"text_body":"Thanks, + we are on it.","html_body":null,"attachments":[],"delivery_status":"delivered","delivered_at":"2026-10-05T14:11:49.792Z","bounced_at":null,"delivery":{"to":"sdk-vcr-customer@sdk-smoke-tests.mailtrap.link","status":"delivered","delivered_at":"2026-10-05T14:11:49.792Z","bounced_at":null}},{"visibility_status":"available","direction":"inbound","id":"1878219302923739200","message_group_id":null,"subject":"Re: + Acme order 20261005141136","rfc_message_id":"","in_reply_to":"","references":[""],"from":"support@sdk-smoke-tests.mailtrap.link","to":["SDK + VCR customer "],"cc":[],"bcc":[],"reply_to":"","created_at":"2026-10-05T14:11:49.776Z","email_size":2791,"text_body":"Thanks, + we are on it.\r\n","html_body":null,"attachments":[],"forwards":[{"rule_id":1,"rule_name":"SDK + VCR copy everything","destination":"welcome@rw.com","status":"forwarded","reason":null,"message_id":"b56ec870-c0c6-11f1-0000-f173219567be"}]},{"visibility_status":"available","direction":"outbound","id":"b56ec870-c0c6-11f1-0000-f173219567be","message_group_id":null,"subject":"Re: + Acme order 20261005141136","rfc_message_id":"","in_reply_to":"","references":[""],"from":"sdk-vcr-customer@sdk-smoke-tests.mailtrap.link","to":["forwarded@example.com"],"cc":[],"bcc":[],"reply_to":"","created_at":"2026-10-05T14:11:50.519Z","email_size":2921,"text_body":"Thanks, + we are on it.\r\n","html_body":null,"attachments":[],"delivery_status":"delivered","delivered_at":"2026-10-05T14:11:51.733Z","bounced_at":null,"delivery":{"to":"forwarded@example.com","status":"delivered","delivered_at":"2026-10-05T14:11:51.733Z","bounced_at":null}}]}' + recorded_at: Mon, 05 Oct 2026 14:16:04 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_list/with_search/passes_search_and_returns_the_matching_threads.yml b/spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_list/with_search/passes_search_and_returns_the_matching_threads.yml new file mode 100644 index 00000000..a4145676 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_InboundThreadsAPI/_list/with_search/passes_search_and_returns_the_matching_threads.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/inbound/inboxes/3924/threads?search=acme + 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: + - Mon, 05 Oct 2026 14:16:04 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Server: + - cloudflare + Vary: + - Accept + - Accept-Encoding + 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 + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Etag: + - W/"3d12e6c75bfc709bc25cd3be5da815f4" + Cache-Control: + - max-age=0, private, must-revalidate + X-Runtime: + - '0.061806' + Strict-Transport-Security: + - max-age=63072000 + Cf-Cache-Status: + - DYNAMIC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"data":[{"id":"1878219290603651072","subject":"Acme order 20261005141136","message_count":5,"size":9134,"first_message_at":"2026-10-05T14:11:38.019Z","last_received_at":"2026-10-05T14:11:49.776Z","last_sent_at":"2026-10-05T14:11:50.519Z","last_activity_at":"2026-10-05T14:11:50.519Z","last_message_id":"1878219303702581248","senders":["SDK + VCR customer ","support@sdk-smoke-tests.mailtrap.link","sdk-vcr-customer@sdk-smoke-tests.mailtrap.link"],"recipients":["support@sdk-smoke-tests.mailtrap.link","SDK + VCR customer ","forwarded@example.com"],"attachments":[]},{"id":"1878219291878346816","subject":"Acme + order 20261005141136","message_count":1,"size":2773,"first_message_at":"2026-10-05T14:11:39.234Z","last_received_at":null,"last_sent_at":"2026-10-05T14:11:39.234Z","last_activity_at":"2026-10-05T14:11:39.234Z","last_message_id":"1878219291869958208","senders":["support@sdk-smoke-tests.mailtrap.link"],"recipients":["forwarded@example.com"],"attachments":[]}],"total_count":2,"last_id":null}' + recorded_at: Mon, 05 Oct 2026 14:16:04 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/mailtrap/inbound_forward_outcome_spec.rb b/spec/mailtrap/inbound_forward_outcome_spec.rb new file mode 100644 index 00000000..c49be332 --- /dev/null +++ b/spec/mailtrap/inbound_forward_outcome_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::InboundForwardOutcome do + describe '#initialize' do + subject(:outcome) do + described_class.new( + rule_id: 7, + rule_name: 'Copy to support team', + destination: 'team@example.com', + status: 'rejected', + reason: 'loop_prevention', + message_id: nil + ) + end + + it 'creates a forward outcome with all attributes' do + expect(outcome).to have_attributes( + rule_id: 7, + rule_name: 'Copy to support team', + destination: 'team@example.com', + status: 'rejected', + reason: 'loop_prevention', + message_id: nil + ) + end + end +end diff --git a/spec/mailtrap/inbound_forward_rule_condition_spec.rb b/spec/mailtrap/inbound_forward_rule_condition_spec.rb new file mode 100644 index 00000000..973eec0d --- /dev/null +++ b/spec/mailtrap/inbound_forward_rule_condition_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::InboundForwardRuleCondition do + describe '#initialize' do + subject(:condition) do + described_class.new(match_type: 'header', operator: 'equal', value: 'high', header_key: 'X-Priority-Level') + end + + it 'creates a condition with all attributes' do + expect(condition).to have_attributes( + match_type: 'header', + operator: 'equal', + value: 'high', + header_key: 'X-Priority-Level' + ) + end + end +end diff --git a/spec/mailtrap/inbound_forward_rule_destination_spec.rb b/spec/mailtrap/inbound_forward_rule_destination_spec.rb new file mode 100644 index 00000000..7f974121 --- /dev/null +++ b/spec/mailtrap/inbound_forward_rule_destination_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::InboundForwardRuleDestination do + describe '#initialize' do + subject(:destination) { described_class.new(email: 'finance@example.com') } + + it 'creates a destination with all attributes' do + expect(destination).to have_attributes(email: 'finance@example.com') + end + end +end diff --git a/spec/mailtrap/inbound_forward_rule_spec.rb b/spec/mailtrap/inbound_forward_rule_spec.rb new file mode 100644 index 00000000..6c7703c5 --- /dev/null +++ b/spec/mailtrap/inbound_forward_rule_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::InboundForwardRule do + describe '#initialize' do + subject(:forward_rule) do + described_class.new( + id: 7, + name: 'Copy billing mail to finance', + created_at: '2026-05-08T10:30:00.000Z', + updated_at: '2026-05-08T12:15:00.000Z', + conditions: [condition], + destinations: [destination] + ) + end + + let(:condition) do + Mailtrap::InboundForwardRuleCondition.new( + match_type: 'sender', operator: 'ends_with', value: '@billing.example.com', header_key: nil + ) + end + let(:destination) { Mailtrap::InboundForwardRuleDestination.new(email: 'finance@example.com') } + + it 'creates a forward rule with all attributes' do + expect(forward_rule).to have_attributes( + id: 7, + name: 'Copy billing mail to finance', + created_at: '2026-05-08T10:30:00.000Z', + updated_at: '2026-05-08T12:15:00.000Z', + conditions: [have_attributes(match_type: 'sender', operator: 'ends_with', value: '@billing.example.com')], + destinations: [have_attributes(email: 'finance@example.com')] + ) + end + end +end diff --git a/spec/mailtrap/inbound_forward_rules_api_spec.rb b/spec/mailtrap/inbound_forward_rules_api_spec.rb new file mode 100644 index 00000000..c7bfbd0a --- /dev/null +++ b/spec/mailtrap/inbound_forward_rules_api_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::InboundForwardRulesAPI, :vcr do + subject(:forward_rules_api) { described_class.new(client) } + + let(:client) { Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY', 'local-api-key')) } + let(:inbox_id) { 3924 } + let(:forward_rule_id) { 2 } + + describe '#list' do + subject(:list) { forward_rules_api.list(inbox_id) } + + it 'maps response data to InboundForwardRule objects' do + expect(list).to all(be_a(Mailtrap::InboundForwardRule)) + end + end + + describe '#get' do + subject(:get) { forward_rules_api.get(inbox_id, forward_rule_id) } + + it 'maps response data to an InboundForwardRule object' do + expect(get).to be_a(Mailtrap::InboundForwardRule) + expect(get).to have_attributes(id: forward_rule_id, name: be_a(String)) + expect(get.conditions).to all(be_a(Mailtrap::InboundForwardRuleCondition)) + expect(get.destinations).to all(be_a(Mailtrap::InboundForwardRuleDestination)) + end + + context 'when the forward rule does not exist' do + let(:forward_rule_id) { -1 } + + it 'raises an error' do + expect { get }.to raise_error(Mailtrap::Error) + end + end + end + + describe '#create' do + subject(:create) { forward_rules_api.create(inbox_id, options) } + + let(:options) do + { + name: 'Copy billing mail to finance', + conditions: [{ match_type: 'sender', operator: 'ends_with', value: '@billing.example.com' }], + destinations: [{ email: 'finance@example.com' }] + } + end + + it 'maps response data to an InboundForwardRule object' do + expect(create).to be_a(Mailtrap::InboundForwardRule) + expect(create).to have_attributes(name: 'Copy billing mail to finance') + expect(create.destinations).to contain_exactly(be_a(Mailtrap::InboundForwardRuleDestination)) + end + + context 'when invalid options are provided' do + let(:options) { { unsupported: 'value' } } + + it 'raises ArgumentError' do + expect { create }.to raise_error(ArgumentError) + end + end + end + + describe '#update' do + subject(:update) { forward_rules_api.update(inbox_id, forward_rule_id, options) } + + let(:options) { { destinations: [{ email: 'finance@example.com' }, { email: 'accounting@example.com' }] } } + + it 'maps response data to an InboundForwardRule object' do + expect(update).to be_a(Mailtrap::InboundForwardRule) + expect(update.destinations.size).to eq(2) + end + + context 'when clearing conditions' do + let(:options) { { conditions: [] } } + + it 'returns the rule without conditions' do + expect(update.conditions).to eq([]) + end + end + + context 'when invalid options are provided' do + let(:options) { { unsupported: 'value' } } + + it 'raises ArgumentError' do + expect { update }.to raise_error(ArgumentError) + end + end + end + + describe '#delete' do + subject(:delete) { forward_rules_api.delete(inbox_id, forward_rule_id) } + + it 'returns nil' do + expect(delete).to be_nil + end + + context 'when the forward rule does not exist' do + let(:forward_rule_id) { -1 } + + it 'raises an error' do + expect { delete }.to raise_error(Mailtrap::Error) + end + end + end +end diff --git a/spec/mailtrap/inbound_message_spec.rb b/spec/mailtrap/inbound_message_spec.rb index 9dc85dbd..4c64f745 100644 --- a/spec/mailtrap/inbound_message_spec.rb +++ b/spec/mailtrap/inbound_message_spec.rb @@ -13,7 +13,8 @@ references: [''], received_at: '2026-01-15T10:30:00Z', thread_id: '1700000000000124', - attachments: [] + attachments: [], + forwards: [] ) end @@ -28,7 +29,8 @@ references: [''], received_at: '2026-01-15T10:30:00Z', thread_id: '1700000000000124', - attachments: [] + attachments: [], + forwards: [] ) end end diff --git a/spec/mailtrap/inbound_messages_api_spec.rb b/spec/mailtrap/inbound_messages_api_spec.rb index 2e782537..07014a2a 100644 --- a/spec/mailtrap/inbound_messages_api_spec.rb +++ b/spec/mailtrap/inbound_messages_api_spec.rb @@ -34,6 +34,15 @@ expect(get.attachments).to all(be_a(Mailtrap::InboundAttachment)) end + context 'with forwards' do + let(:inbox_id) { 3924 } + let(:message_id) { '1878219290673225792' } + + it 'maps forwards to InboundForwardOutcome objects' do + expect(get.forwards).to include(be_a(Mailtrap::InboundForwardOutcome)) + end + end + context 'when the message does not exist' do let(:message_id) { 'does-not-exist' } diff --git a/spec/mailtrap/inbound_thread_message_delivery_spec.rb b/spec/mailtrap/inbound_thread_message_delivery_spec.rb new file mode 100644 index 00000000..c2794bd7 --- /dev/null +++ b/spec/mailtrap/inbound_thread_message_delivery_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::InboundThreadMessageDelivery do + describe '#initialize' do + subject(:delivery) do + described_class.new( + to: 'customer@example.com', + status: 'delivered', + delivered_at: '2026-05-08T11:40:05.000Z', + bounced_at: nil + ) + end + + it 'creates a delivery with all attributes' do + expect(delivery).to have_attributes( + to: 'customer@example.com', + status: 'delivered', + delivered_at: '2026-05-08T11:40:05.000Z', + bounced_at: nil + ) + end + end +end diff --git a/spec/mailtrap/inbound_thread_message_spec.rb b/spec/mailtrap/inbound_thread_message_spec.rb index 38c288a2..728ec576 100644 --- a/spec/mailtrap/inbound_thread_message_spec.rb +++ b/spec/mailtrap/inbound_thread_message_spec.rb @@ -8,8 +8,13 @@ direction: 'outbound', id: '1700000000000125', subject: 'Re: Question', - delivery_status: 'delivered', - delivered_at: '2026-01-15T10:31:00Z' + delivery: + ) + end + + let(:delivery) do + Mailtrap::InboundThreadMessageDelivery.new( + to: 'customer@example.com', status: 'delivered', delivered_at: '2026-01-15T10:31:00Z', bounced_at: nil ) end @@ -19,8 +24,7 @@ direction: 'outbound', id: '1700000000000125', subject: 'Re: Question', - delivery_status: 'delivered', - delivered_at: '2026-01-15T10:31:00Z' + delivery: have_attributes(to: 'customer@example.com', status: 'delivered') ) end end diff --git a/spec/mailtrap/inbound_threads_api_spec.rb b/spec/mailtrap/inbound_threads_api_spec.rb index 28e59659..850b2d16 100644 --- a/spec/mailtrap/inbound_threads_api_spec.rb +++ b/spec/mailtrap/inbound_threads_api_spec.rb @@ -23,6 +23,17 @@ expect(list).to be_a(Mailtrap::InboundThreadsListResponse) end end + + context 'with search' do + subject(:list) { threads_api.list(inbox_id, search: 'acme') } + + let(:inbox_id) { 3924 } + + it 'passes search and returns the matching threads' do + expect(list).to be_a(Mailtrap::InboundThreadsListResponse) + expect(list.data).to all(be_a(Mailtrap::InboundThread)) + end + end end describe '#get' do @@ -34,6 +45,18 @@ expect(get.messages).to all(be_a(Mailtrap::InboundThreadMessage)) end + context 'with a forwarded message and a sent reply' do + let(:inbox_id) { 3924 } + let(:thread_id) { '1878219290603651072' } + + it 'maps delivery on sent messages and forwards on received messages' do + sent, received = get.messages.partition { |m| m.direction == 'outbound' } + + expect(sent.map(&:delivery)).to all(be_a(Mailtrap::InboundThreadMessageDelivery)) + expect(received.flat_map(&:forwards)).to include(be_a(Mailtrap::InboundForwardOutcome)) + end + end + context 'when the thread does not exist' do let(:thread_id) { 'does-not-exist' } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b7257f8a..036f3194 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,6 +29,11 @@ interaction.response.body.gsub!(/"username":"[^"]*"/, '"username": "railsware"') interaction.response.body.gsub!(/"password":"[^"]*"/, '"password": "xxxxxxxx"') interaction.response.body.gsub!(/"email":"[^"]*"/, '"email": "welcome@rw.com"') + interaction.response.body.gsub!(/"destination":"[^"]*"/, '"destination":"welcome@rw.com"') + interaction.response.body.gsub!( + /"(raw_message_url|download_url)":"[^"]*X-Amz-[^"]*"/, + '"\1":"https://example.com/signed-url"' + ) interaction.response.body.gsub!( /"forward_from_email_address":"[^"]*"/, '"forward_from_email_address": "railsware@forward.mailtrap.info"'