diff --git a/examples/batch.rb b/examples/batch.rb new file mode 100644 index 0000000..da353de --- /dev/null +++ b/examples/batch.rb @@ -0,0 +1,105 @@ +require 'mailtrap' +require 'base64' + +client = Mailtrap::Client.new(api_key: 'your-api-key') + +# Set your API credentials as environment variables +# export MAILTRAP_API_KEY='your-api-key' +# +# client = Mailtrap::Client.new +# Bulk sending (@see https://help.mailtrap.io/article/113-sending-streams) +# client = Mailtrap::Client.new(bulk: true) +# Sandbox sending (@see https://help.mailtrap.io/article/109-getting-started-with-mailtrap-email-testing) +# client = Mailtrap::Client.new(sandbox: true, inbox_id: 12) + +# Batch sending with Mailtrap::Mail::Base +mail = Mailtrap::Mail.batch_base_from_content( + from: { email: 'mailtrap@demomailtrap.co', name: 'Mailtrap Test' }, + subject: 'You are awesome!', + text: 'Congrats for sending test email with Mailtrap!', + category: 'Integration Test', + # attachments: [ + # { + # content: Base64.encode64('Attachment content'), # base64 encoded content or IO string + # filename: 'attachment.txt' + # } + # ], + headers: { + 'X-MT-Header': 'Custom header' + }, + custom_variables: { + year: 2022 + } +) + +client.send_batch( + mail, [ + Mailtrap::Mail.from_content( + to: [ + { email: 'your@email.com', name: 'recipient1' } + ] + ), + Mailtrap::Mail::Base.new( + to: [ + { email: 'your@email.com', name: 'recipient2' } + ] + ) + ] +) + +# Batch sending with Mailtrap::Mail::Base +mail = Mailtrap::Mail.batch_base_from_template( + from: { email: 'mailtrap@demomailtrap.co', name: 'Mailtrap Test' }, + reply_to: { email: 'support@example.com', name: 'Mailtrap Reply-To' }, + template_uuid: '339c8ab0-e73c-4269-984e-0d2446aacf2c', + template_variables: { + 'user_name' => 'John Doe' + } +) + +client.send_batch( + mail, [ + Mailtrap::Mail::Base.new( + to: [ + { email: 'your@email.com', name: 'recipient1' } + ] + ), + Mailtrap::Mail::Base.new( + to: [ + { email: 'your@email.com', name: 'recipient2' } + ], + template_variables: { + 'user_name' => 'John Doe 1', + 'user_name2' => 'John Doe 2' + } + ) + ] +) + +# You can also pass the request parameters directly +client.send_batch( + { + from: { email: 'mailtrap@demomailtrap.co', name: 'Mailtrap Test' }, + reply_to: { email: 'support@example.com', name: 'Mailtrap Reply-To' }, + template_uuid: '339c8ab0-e73c-4269-984e-0d2446aacf2c', + template_variables: { + 'user_name' => 'John Doe' + }, + }, + [ + { + to: [ + { email: 'your@email.com', name: 'recipient1' } + ] + }, + { + to: [ + { email: 'your@email.com', name: 'recipient2' } + ], + template_variables: { + 'user_name' => 'John Doe 1', + 'user_name2' => 'John Doe 2' + } + } + ] +) diff --git a/lib/mailtrap/client.rb b/lib/mailtrap/client.rb index 9e4de51..a26d0ff 100644 --- a/lib/mailtrap/client.rb +++ b/lib/mailtrap/client.rb @@ -37,11 +37,9 @@ def initialize( # rubocop:disable Metrics/ParameterLists sandbox: false, inbox_id: nil ) - raise ArgumentError, 'api_key is required' if api_key.nil? - raise ArgumentError, 'api_port is required' if api_port.nil? + validate_args!(api_key, api_port, bulk, sandbox, inbox_id) api_host ||= select_api_host(bulk:, sandbox:) - raise ArgumentError, 'inbox_id is required for sandbox API' if sandbox && inbox_id.nil? @api_key = api_key @api_host = api_host @@ -53,6 +51,76 @@ def initialize( # rubocop:disable Metrics/ParameterLists @http_clients = {} end + # Sends a batch of emails. + # @example Sending batch emails using helpers + # mail = Mailtrap::Mail.batch_base_from_template( + # from: { email: 'mailtrap@demomailtrap.co', name: 'Mailtrap Test' }, + # reply_to: { email: 'support@example.com', name: 'Mailtrap Reply-To' }, + # template_uuid: '339c8ab0-e73c-4269-984e-0d2446aacf2c', + # template_variables: { + # 'user_name' => 'John Doe' + # } + # ) + # + # client.send_batch( + # mail, + # [ + # Mailtrap::Mail.from_content( + # to: [ + # { email: 'your@email.com', name: 'recipient1' } + # ] + # ), + # Mailtrap::Mail.from_template( + # to: [ + # { email: 'your@email.com', name: 'recipient2' } + # ], + # template_variables: { + # 'user_name' => 'John Doe 1', + # 'user_name2' => 'John Doe 2' + # } + # ) + # ] + # ) + # + # @example Sending batch emails using plain hashes + # client.send_batch( + # { + # from: { email: 'mailtrap@demomailtrap.co', name: 'Mailtrap Test' }, + # reply_to: { email: 'support@example.com', name: 'Mailtrap Reply-To' }, + # template_uuid: '339c8ab0-e73c-4269-984e-0d2446aacf2c', + # template_variables: { + # 'user_name' => 'John Doe' + # } + # }, + # [ + # { + # to: [ + # { email: 'your@email.com', name: 'recipient1' } + # ] + # }, + # { + # to: [ + # { email: 'your@email.com', name: 'recipient2' } + # ], + # template_variables: { + # 'user_name' => 'John Doe 1', + # 'user_name2' => 'John Doe 2' + # } + # } + # ] + # ) + # @param base [#to_json] The base email configuration for the batch. + # @param requests [Array<#to_json>] Array of individual email requests. + # @return [Hash] The JSON response from the API. + # @!macro api_errors + # @raise [Mailtrap::MailSizeError] If the message is too large. + def send_batch(base, requests) + perform_request(:post, api_host, batch_request_path, { + base:, + requests: + }) + end + # Sends an email # @example # mail = Mailtrap::Mail.from_template( @@ -124,8 +192,6 @@ def http_client_for(host) end def select_api_host(bulk:, sandbox:) - raise ArgumentError, 'bulk mode is not applicable for sandbox API' if bulk && sandbox - if sandbox SANDBOX_API_HOST elsif bulk @@ -139,6 +205,10 @@ def send_path "/api/send#{"/#{inbox_id}" if sandbox}" end + def batch_request_path + "/api/batch#{"/#{inbox_id}" if sandbox}" + end + def perform_request(method, host, path, body = nil) http_client = http_client_for(host) request = setup_request(method, path, body) @@ -203,5 +273,12 @@ def response_errors(body) def json_response(body) JSON.parse(body, symbolize_names: true) end + + def validate_args!(api_key, api_port, bulk, sandbox, inbox_id) + raise ArgumentError, 'api_key is required' if api_key.nil? + raise ArgumentError, 'api_port is required' if api_port.nil? + raise ArgumentError, 'bulk stream is not applicable for sandbox API' if bulk && sandbox + raise ArgumentError, 'inbox_id is required for sandbox API' if sandbox && inbox_id.nil? + end end end diff --git a/lib/mailtrap/mail.rb b/lib/mailtrap/mail.rb index 7ecfe55..dec7d02 100644 --- a/lib/mailtrap/mail.rb +++ b/lib/mailtrap/mail.rb @@ -117,6 +117,75 @@ def from_content( # rubocop:disable Metrics/ParameterLists ) end + # Builds a base mail object for batch sending using a pre-defined email template. + # This "base" defines shared properties (such as sender, reply-to, template UUID, and template variables) + # that will be used as defaults for all emails in the batch. Individual batch requests can override these values. + # Use this method when you want to send multiple emails with similar content, leveraging a template defined in the Mailtrap dashboard. # rubocop:disable Layout/LineLength + # Template variables can be passed to customize the template content for all recipients, and can be overridden per request. # rubocop:disable Layout/LineLength + # @example + # base_mail = Mailtrap::Mail.batch_base_from_template( + # from: { email: 'mailtrap@example.com', name: 'Mailtrap Test' }, + # template_uuid: '2f45b0aa-bbed-432f-95e4-e145e1965ba2', + # template_variables: { + # 'user_name' => 'John Doe' + # } + # ) + # # Use base_mail as the base for batch sending with Mailtrap::Client#send_batch + def batch_base_from_template( # rubocop:disable Metrics/ParameterLists + from: nil, + reply_to: nil, + attachments: [], + headers: {}, + custom_variables: {}, + template_uuid: nil, + template_variables: {} + ) + Mailtrap::Mail::Base.new( + from:, + reply_to:, + attachments:, + headers:, + custom_variables:, + template_uuid:, + template_variables: + ) + end + + # Builds a base mail object for batch sending with custom content (subject, text, html, category). + # This "base" defines shared properties for all emails in the batch, such as sender, subject, and body. + # Individual batch requests can override these values as needed. + # Use this method when you want to send multiple emails with similar custom content to different recipients. + # @example + # base_mail = Mailtrap::Mail.batch_base_from_content( + # from: { email: 'mailtrap@example.com', name: 'Mailtrap Test' }, + # subject: 'You are awesome!', + # text: 'Congrats for sending test email with Mailtrap!' + # ) + # # Use base_mail as the base for batch sending with Mailtrap::Client#send_batch + def batch_base_from_content( # rubocop:disable Metrics/ParameterLists + from: nil, + reply_to: nil, + attachments: [], + headers: {}, + custom_variables: {}, + subject: nil, + text: nil, + html: nil, + category: nil + ) + Mailtrap::Mail::Base.new( + from:, + reply_to:, + attachments:, + headers:, + custom_variables:, + subject:, + text:, + html:, + category: + ) + end + # Builds a mail object from Mail::Message # @param message [Mail::Message] # @return [Mailtrap::Mail::Base] diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/when_in_bulk_stream/successfully_sends_a_batch_of_emails.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/when_in_bulk_stream/successfully_sends_a_batch_of_emails.yml new file mode 100644 index 0000000..0c05c2c --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/when_in_bulk_stream/successfully_sends_a_batch_of_emails.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: https://bulk.api.mailtrap.io/api/batch + body: + encoding: UTF-8 + string: '{"base":{"from":{"email":"mailtrap@demomailtrap.co","name":"Mailtrap"},"subject":"Batch Subject","text":"Batch Text"},"requests":[{"to":[{"email":"to@mail.com","name":"recipient1"}]},{"to":[{"email":"to@mail.com","name":"recipient2"}]}]}' + 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: + - Wed, 02 Jul 2025 07:56:41 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=0 + Server: + - cloudflare + Cf-Ray: + - 958c9686a8ccc400-WAW + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"success":true,"responses":[{"success":true,"message_ids":["17287920-571a-11f0-0040-f13664d022fb"]},{"success":true,"message_ids":["17287920-571a-11f0-0041-f13664d022fb"]}]}' + recorded_at: Wed, 02 Jul 2025 07:56:41 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/when_in_sandbox_mode/successfully_sends_a_batch_of_emails.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/when_in_sandbox_mode/successfully_sends_a_batch_of_emails.yml new file mode 100644 index 0000000..847bd10 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/when_in_sandbox_mode/successfully_sends_a_batch_of_emails.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.api.mailtrap.io/api/batch/3861666 + body: + encoding: UTF-8 + string: '{"base":{"from":{"email":"mailtrap@demomailtrap.co","name":"Mailtrap"},"subject":"Batch Subject","text":"Batch Text"},"requests":[{"to":[{"email":"to@mail.com","name":"recipient1"}]},{"to":[{"email":"to@mail.com","name":"recipient2"}]}]}' + 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: + - Wed, 02 Jul 2025 07:54:12 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=0 + Server: + - cloudflare + Cf-Ray: + - 958c92e2cbe6da34-WAW + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"success":true,"responses":[{"success":true,"message_ids":["4967738534"]},{"success":true,"message_ids":["4967738533"]}]}' + recorded_at: Wed, 02 Jul 2025 07:54:12 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_API_errors/handles_API_errors.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_API_errors/handles_API_errors.yml new file mode 100644 index 0000000..1abb985 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_API_errors/handles_API_errors.yml @@ -0,0 +1,49 @@ +--- +http_interactions: +- request: + method: post + uri: https://bulk.api.mailtrap.io/api/batch + body: + encoding: UTF-8 + string: '{"base":{"text":"Batch Text"},"requests":[{"to":[{"email":"to@mail.com","name":"recipient1"}]},{"to":[{"email":"to@mail.com","name":"recipient2"}]}]}' + 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: + - Wed, 02 Jul 2025 07:54:58 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=0 + Server: + - cloudflare + Cf-Ray: + - 958c9402bfe0bf31-WAW + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"success":true,"responses":[{"success":false,"errors":["''from'' is + required","''subject'' is required"]},{"success":false,"errors":["''from'' + is required","''subject'' is required"]}]}' + recorded_at: Wed, 02 Jul 2025 07:54:58 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_template/successfully_sends_a_batch_of_emails_with_template.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_template/successfully_sends_a_batch_of_emails_with_template.yml new file mode 100644 index 0000000..ad2c848 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send_batch/with_template/successfully_sends_a_batch_of_emails_with_template.yml @@ -0,0 +1,47 @@ +--- +http_interactions: +- request: + method: post + uri: https://bulk.api.mailtrap.io/api/batch + body: + encoding: UTF-8 + string: '{"base":{"from":{"email":"mailtrap@demomailtrap.co","name":"Mailtrap"},"template_uuid":"be5ed4dd-b374-4856-928d-f0957304123d","template_variables":{"company_name":"Mailtrap"}},"requests":[{"to":[{"email":"to@mail.com","name":"recipient1"}]},{"to":[{"email":"to@mail.com","name":"recipient2"}]}]}' + 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: + - Wed, 02 Jul 2025 07:56:45 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cf-Cache-Status: + - DYNAMIC + Strict-Transport-Security: + - max-age=0 + Server: + - cloudflare + Cf-Ray: + - 958c969fbd32eebe-WAW + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"success":true,"responses":[{"success":true,"message_ids":["19922620-571a-11f0-0040-f13664d022fb"]},{"success":true,"message_ids":["19922620-571a-11f0-0041-f13664d022fb"]}]}' + recorded_at: Wed, 02 Jul 2025 07:56:45 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/mailtrap/client_spec.rb b/spec/mailtrap/client_spec.rb index cd8375a..65d8a44 100644 --- a/spec/mailtrap/client_spec.rb +++ b/spec/mailtrap/client_spec.rb @@ -110,7 +110,7 @@ described_class.new(api_key:, bulk: true, sandbox: true) end - it { expect { send }.to raise_error(ArgumentError, 'bulk mode is not applicable for sandbox API') } + it { expect { send }.to raise_error(ArgumentError, 'bulk stream is not applicable for sandbox API') } end end @@ -265,4 +265,142 @@ def stub_post(path, status, body) end end end + + describe '#send_batch' do + let(:api_key) { ENV.fetch('MAILTRAP_API_KEY', 'correct-api-key') } + let(:base_mail) do + Mailtrap::Mail::Base.new( + from: { + email: 'mailtrap@demomailtrap.co', + name: 'Mailtrap' + }, + subject: 'Batch Subject', + text: 'Batch Text' + ) + end + let(:recipients) do + [ + Mailtrap::Mail::Base.new( + to: [ + { + email: ENV.fetch('MAILTRAP_TO_EMAIL', 'to@mail.com'), + name: 'recipient1' + } + ] + ), + Mailtrap::Mail::Base.new( + to: [ + { + email: ENV.fetch('MAILTRAP_TO_EMAIL', 'to@mail.com'), + name: 'recipient2' + } + ] + ) + ] + end + + context 'when bulk and sandbox modes are used together' do + let(:client) do + described_class.new( + api_key:, + bulk: true, + sandbox: true + ) + end + + it 'raises an error' do + expect do + client.send_batch(base_mail, recipients) + end.to raise_error(ArgumentError, 'bulk stream is not applicable for sandbox API') + end + end + + context 'when in bulk stream' do + let(:client) { described_class.new(api_key:, bulk: true) } + + it 'successfully sends a batch of emails', :vcr do + response = client.send_batch(base_mail, recipients) + expect(response).to include( + success: true, + responses: array_including( + hash_including( + success: true, + message_ids: array_including(kind_of(String)) + ) + ) + ) + end + end + + context 'when in sandbox mode' do + let(:client) { described_class.new(api_key:, sandbox: true, inbox_id: 3_861_666) } + + it 'successfully sends a batch of emails', :vcr do + response = client.send_batch(base_mail, recipients) + expect(response).to include( + success: true, + responses: array_including( + hash_including( + success: true, + message_ids: array_including(kind_of(String)) + ) + ) + ) + end + end + + context 'with template' do + let(:client) { described_class.new(api_key:, bulk: true) } + let(:template_mail) do + Mailtrap::Mail::Base.new( + from: { + email: 'mailtrap@demomailtrap.co', + name: 'Mailtrap' + }, + template_uuid: ENV.fetch('MAILTRAP_TEMPLATE_UUID', 'be5ed4dd-b374-4856-928d-f0957304123d'), + template_variables: { + company_name: 'Mailtrap' + } + ) + end + + it 'successfully sends a batch of emails with template', :vcr do + response = client.send_batch(template_mail, recipients) + expect(response).to include( + success: true, + responses: array_including( + hash_including( + success: true, + message_ids: array_including(kind_of(String)) + ) + ) + ) + end + end + + context 'with API errors' do + let(:client) { described_class.new(api_key:, bulk: true) } + let(:invalid_mail) do + Mailtrap::Mail::Base.new( + text: 'Batch Text' + ) + end + + it 'handles API errors', :vcr do + response = client.send_batch(invalid_mail, recipients) + expect(response).to include( + success: true, + responses: array_including( + hash_including( + success: false, + errors: array_including( + "'from' is required", + "'subject' is required" + ) + ) + ) + ) + end + end + end end