diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index 3b6af23..07e7e5c 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -11,6 +11,7 @@ require_relative 'mailtrap/contact_imports_api' require_relative 'mailtrap/suppressions_api' require_relative 'mailtrap/projects_api' +require_relative 'mailtrap/sandbox_attachments_api' module Mailtrap # @!macro api_errors diff --git a/lib/mailtrap/sandbox_attachment.rb b/lib/mailtrap/sandbox_attachment.rb new file mode 100644 index 0000000..d68b7e5 --- /dev/null +++ b/lib/mailtrap/sandbox_attachment.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Mailtrap + # Data Transfer Object for SandboxAttachment + # @see https://docs.mailtrap.io/developers/email-sandbox/email-sandbox-api/attachments + # @attr_reader id [Integer] The project ID + # @attr_reader message_id [Integer] The message ID + # @attr_reader filename [String] The attachment filename + # @attr_reader attachment_type [String] The attachment type + # @attr_reader content_type [String] The attachment content type + # @attr_reader content_id [String] The attachment content ID + # @attr_reader transfer_encoding [String] The attachment transfer encoding + # @attr_reader attachment_size [Integer] The attachment size in bytes + # @attr_reader created_at [String] The attachment creation timestamp + # @attr_reader updated_at [String] The attachment update timestamp + # @attr_reader attachment_human_size [String] The attachment size in human-readable format + # @attr_reader download_path [String] The attachment download path + # + SandboxAttachment = Struct.new( + :id, + :message_id, + :filename, + :attachment_type, + :content_type, + :content_id, + :transfer_encoding, + :attachment_size, + :created_at, + :updated_at, + :attachment_human_size, + :download_path, + keyword_init: true + ) do + # @return [Hash] The Project attributes as a hash + def to_h + super.compact + end + end +end diff --git a/lib/mailtrap/sandbox_attachments_api.rb b/lib/mailtrap/sandbox_attachments_api.rb new file mode 100644 index 0000000..077bec0 --- /dev/null +++ b/lib/mailtrap/sandbox_attachments_api.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require_relative 'base_api' +require_relative 'sandbox_attachment' + +module Mailtrap + class SandboxAttachmentsAPI + include BaseAPI + + self.response_class = SandboxAttachment + + # Retrieves a specific sandbox attachment + # @param inbox_id [Integer] The inbox ID + # @param sandbox_message_id [Integer] The sandbox message ID + # @param sandbox_attachment_id [Integer] The sandbox attachment ID + # @return [SandboxAttachment] Sandbox attachment object + # @!macro api_errors + def get(inbox_id, sandbox_message_id, sandbox_attachment_id) + response = client.get( + "#{base_path}/inboxes/#{inbox_id}/messages/#{sandbox_message_id}/attachments/#{sandbox_attachment_id}" + ) + handle_response(response) + end + + # Lists all sandbox messages for the account, limited up to 30 at once + # @param inbox_id [Integer] The inbox ID + # @param sandbox_message_id [Integer] The sandbox message ID + # @return [Array] Array of sandbox message objects + # @!macro api_errors + def list(inbox_id, sandbox_message_id) + response = client.get("#{base_path}/inboxes/#{inbox_id}/messages/#{sandbox_message_id}/attachments") + response.map { |item| handle_response(item) } + end + + private + + def base_path + "/api/accounts/#{account_id}" + end + end +end diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_get/maps_response_data_to_SandboxAttachment_object.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_get/maps_response_data_to_SandboxAttachment_object.yml new file mode 100644 index 0000000..222b12d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_get/maps_response_data_to_SandboxAttachment_object.yml @@ -0,0 +1,82 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/inboxes/4288340/messages/5274457639/attachments/790295400 + 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: + - Wed, 31 Dec 2025 11:16:01 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 + Etag: + - W/"20a77c561cdec81647c3b14d5fef5219" + Last-Modified: + - Wed, 31 Dec 2025 10:44:19 GMT + Vary: + - Accept + Cache-Control: + - max-age=0, private, must-revalidate + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + X-Request-Id: + - c44d7c43-82f2-4b72-a2ee-1271c123043d + X-Runtime: + - '0.027467' + X-Cloud-Trace-Context: + - ecebe3ad6d3246858d22fecd3f4518e3;o=0 + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 9b928fa38c9ae7b4-FRA + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":790295400,"message_id":5274457639,"filename":"example_2.txt","attachment_type":"attachment","content_type":"application/octet-stream","content_id":"","transfer_encoding":"base64","attachment_size":9,"created_at":"2026-01-05T10:44:19.915Z","updated_at":"2026-01-05T10:44:19.915Z","attachment_human_size":"9 + Bytes","download_path":"/api/testing_message_parts/QEVuQwFAEADbV34oPszsE53vA%2FO6HEkR7zkAak6XtHMNostw8J7Jn4ZGVywFbPzBDyYyWrfpm9ZjSvpIGcQfj%2FfEUcM9NQXo0qUcdURrLINzsk+umUeKXdHF2vSM8n8se5cGms9onw7h+uKmi42Lt+dlI7gfyA6+D5sJgq8E%2FUnn8y+1R6PGrQ==/attachment_download"}' + recorded_at: Wed, 31 Dec 2025 11:16:01 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_get/when_attachment_does_not_exist/raises_not_found_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_get/when_attachment_does_not_exist/raises_not_found_error.yml new file mode 100644 index 0000000..ca3575d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_get/when_attachment_does_not_exist/raises_not_found_error.yml @@ -0,0 +1,77 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/inboxes/4288340/messages/5274457639/attachments/-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: + - Wed, 31 Dec 2025 11:16:01 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' + Cache-Control: + - no-cache + X-Request-Id: + - 8246efa1-9916-4958-adb0-9fd9fa949a81 + X-Runtime: + - '0.024542' + X-Cloud-Trace-Context: + - a0c1f68c054c4251812b9bb1f0e0cc60;o=0 + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 9b928fa68d4cdc96-FRA + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"error":"Not Found"}' + recorded_at: Wed, 31 Dec 2025 11:16:01 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_list/maps_response_data_to_SandboxAttachment_objects.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_list/maps_response_data_to_SandboxAttachment_objects.yml new file mode 100644 index 0000000..1430dcb --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_list/maps_response_data_to_SandboxAttachment_objects.yml @@ -0,0 +1,84 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/inboxes/4288340/messages/5274457639/attachments + 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: + - Wed, 31 Dec 2025 11:15:17 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 + Etag: + - W/"889e10c37e4ed6d2d20bde6038207b7a" + Last-Modified: + - Wed, 31 Dec 2025 10:44:19 GMT + Cache-Control: + - max-age=0, private, must-revalidate + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + X-Request-Id: + - 415a147d-251c-4cc8-9f02-07e4ffdcd8b2 + X-Runtime: + - '0.029720' + X-Cloud-Trace-Context: + - a369564f2e084d0c866eb9092f1cf0d6;o=0 + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 9b928e8dcac29bd0-FRA + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '[{"id":790295400,"message_id":5274457639,"filename":"example_2.txt","attachment_type":"attachment","content_type":"application/octet-stream","content_id":"","transfer_encoding":"base64","attachment_size":9,"created_at":"2026-01-05T10:44:19.915Z","updated_at":"2026-01-05T10:44:19.915Z","attachment_human_size":"9 + Bytes","download_path":"/api/testing_message_parts/QEVuQwFAEACixobzUmrJNY+KT5L8UUkg+k9FAWP2RDrWZp+ZEisZmau7qFIg38B6nQdqghtYIHzGRRCKcdkqpmF+PbGGSf10PxzR8i0H3DY6k8YVWs+01%2F7nWHA10xA5zBSP7dkFpt0b4y0JihqDvAPEmvxnMyU67Tc%2FHu3HpgmfcPw1UDq3ag==/attachment_download"},{"id":790295399,"message_id":5274457639,"filename":"example.txt","attachment_type":"attachment","content_type":"application/octet-stream","content_id":"","transfer_encoding":"base64","attachment_size":9,"created_at":"2026-01-05T10:44:19.893Z","updated_at":"2026-01-05T10:44:19.893Z","attachment_human_size":"9 + Bytes","download_path":"/api/testing_message_parts/QEVuQwFAEAAFj4E1onOmSa%2FI4FadW%2FqPUD%2F87dOIguUfn2QeMKayajWVAJMGNYg1Nhag4hP2SWbwfcBEF1hQfL2z4hE22CSUe2hjhJsWuQ1ALH6H9uExLFi2L1YNqBk3Vzff%2FM4fekaz3lVxNzqiTCTked69gOxminZi+GbmoVhabGKh2768QA==/attachment_download"}]' + recorded_at: Wed, 31 Dec 2025 11:15:17 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml new file mode 100644 index 0000000..64dfa46 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_SandboxAttachmentsAPI/_list/when_api_key_is_incorrect/raises_authorization_error.yml @@ -0,0 +1,79 @@ +--- +http_interactions: +- request: + method: get + uri: https://mailtrap.io/api/accounts/1111111/inboxes/4288340/messages/5274457639/attachments + body: + encoding: US-ASCII + string: '' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - mailtrap-ruby (https://github.com/mailtrap/mailtrap-ruby) + Host: + - mailtrap.io + Authorization: + - Bearer + Content-Type: + - application/json + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Wed, 31 Dec 2025 11:15:17 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '31' + Connection: + - keep-alive + Server: + - cloudflare + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Permitted-Cross-Domain-Policies: + - none + Referrer-Policy: + - strict-origin-when-cross-origin + Www-Authenticate: + - Token realm="Application" + Vary: + - Accept + X-Mailtrap-Version: + - v2 + X-Ratelimit-Limit: + - '150' + X-Ratelimit-Remaining: + - '149' + Cache-Control: + - no-cache + X-Request-Id: + - 6cb848c4-20f9-41d2-bdc8-e3246b74d1d8 + X-Runtime: + - '0.012524' + X-Cloud-Trace-Context: + - d49ff377d08640db8f54ed5869ae1ec0;o=0 + Strict-Transport-Security: + - max-age=0 + Cf-Cache-Status: + - DYNAMIC + Cf-Ray: + - 9b928e90ca5665b2-FRA + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: UTF-8 + string: '{"error":"Incorrect API token"}' + recorded_at: Wed, 31 Dec 2025 11:15:17 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/mailtrap/sandbox_attachment_spec.rb b/spec/mailtrap/sandbox_attachment_spec.rb new file mode 100644 index 0000000..2767590 --- /dev/null +++ b/spec/mailtrap/sandbox_attachment_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::SandboxAttachment do + describe '#initialize' do + subject(:sandbox_attachment) { described_class.new(attributes) } + + let(:attributes) do + { + id: 1, + message_id: 2, + filename: 'example.txt', + attachment_type: 'text/plain', + content_type: 'text/plain', + content_id: 'content-id-123', + transfer_encoding: 'base64', + attachment_size: 1024, + created_at: '2024-01-01T12:00:00Z', + updated_at: '2024-01-02T12:00:00Z', + attachment_human_size: '1 KB', + download_path: '/downloads/example.txt' + } + end + + it 'creates a attachment with all attributes' do + expect(sandbox_attachment).to match_struct( + id: 1, + message_id: 2, + filename: 'example.txt', + attachment_type: 'text/plain', + content_type: 'text/plain', + content_id: 'content-id-123', + transfer_encoding: 'base64', + attachment_size: 1024, + created_at: '2024-01-01T12:00:00Z', + updated_at: '2024-01-02T12:00:00Z', + attachment_human_size: '1 KB', + download_path: '/downloads/example.txt' + ) + end + end + + describe '#to_h' do + subject(:hash) { sandbox_attachment.to_h } + + let(:sandbox_attachment) do + described_class.new( + id: 1, + message_id: 2, + filename: 'example.txt', + attachment_type: 'text/plain', + content_type: 'text/plain', + content_id: 'content-id-123', + transfer_encoding: 'base64', + attachment_size: 1024, + created_at: '2024-01-01T12:00:00Z', + updated_at: '2024-01-02T12:00:00Z', + attachment_human_size: '1 KB', + download_path: '/downloads/example.txt' + ) + end + + it 'returns a hash with all attributes' do + expect(hash).to eq( + id: 1, + message_id: 2, + filename: 'example.txt', + attachment_type: 'text/plain', + content_type: 'text/plain', + content_id: 'content-id-123', + transfer_encoding: 'base64', + attachment_size: 1024, + created_at: '2024-01-01T12:00:00Z', + updated_at: '2024-01-02T12:00:00Z', + attachment_human_size: '1 KB', + download_path: '/downloads/example.txt' + ) + end + + context 'when some attributes are nil' do + let(:sandbox_attachment) do + described_class.new( + id: 1 + ) + end + + it 'returns a hash with only non-nil attributes' do + expect(hash).to eq( + id: 1 + ) + end + end + end +end diff --git a/spec/mailtrap/sandbox_attachments_api_spec.rb b/spec/mailtrap/sandbox_attachments_api_spec.rb new file mode 100644 index 0000000..79958f4 --- /dev/null +++ b/spec/mailtrap/sandbox_attachments_api_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +RSpec.describe Mailtrap::SandboxAttachmentsAPI, :vcr do + subject(:sandbox_attachments_api) { described_class.new(account_id, client) } + + let(:account_id) { ENV.fetch('MAILTRAP_ACCOUNT_ID', 1_111_111) } + let(:client) { Mailtrap::Client.new(api_key: ENV.fetch('MAILTRAP_API_KEY', 'local-api-key')) } + let(:inbox_id) { ENV.fetch('MAILTRAP_INBOX_ID', 4_288_340) } + let(:message_id) { ENV.fetch('MAILTRAP_SANDBOX_MESSAGE_ID', 5_274_457_639) } + + describe '#list' do + subject(:list) { sandbox_attachments_api.list(inbox_id, message_id) } + + it 'maps response data to SandboxAttachment objects' do + expect(list).to all(be_a(Mailtrap::SandboxAttachment)) + 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 + + describe '#get' do + subject(:get) { sandbox_attachments_api.get(inbox_id, message_id, attachment_id) } + + let(:attachment_id) { 790_295_400 } + + it 'maps response data to SandboxAttachment object' do + expect(get).to be_a(Mailtrap::SandboxAttachment) + expect(get).to have_attributes( + id: attachment_id, + filename: 'example_2.txt' + ) + end + + context 'when attachment does not exist' do + let(:attachment_id) { -1 } + + it 'raises not found error' do + expect { get }.to raise_error do |error| + expect(error).to be_a(Mailtrap::Error) + expect(error.message).to include('Not Found') + expect(error.messages.any? { |msg| msg.include?('Not Found') }).to be true + end + end + end + end +end