From 705b4a201bcfbebf04fa034816ed2a66a0a073af Mon Sep 17 00:00:00 2001 From: Jan Pieczul <30315254+Artheanos@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:59:17 +0100 Subject: [PATCH] Add testing api support (#20) * Add Sending::Client enhancements for bulk and sandbox API support (w/o tests) * Remove Sending module * Add tests for bulk and sandbox * Updated CHANGELOG.md * Create examples folder * Fixes * Exclude examples folder from gem build * Add a test case * Fixes * Change version --------- Co-authored-by: Victor Zagorodny --- .rubocop.yml | 8 ++ CHANGELOG.md | 16 +++ Gemfile.lock | 2 +- README.md | 100 +++------------ examples/action_mailer.rb | 18 +++ examples/email_template.rb | 17 +++ examples/full.rb | 48 +++++++ lib/mailtrap.rb | 2 +- lib/mailtrap/action_mailer/delivery_method.rb | 4 +- lib/mailtrap/attachment.rb | 50 ++++++++ lib/mailtrap/client.rb | 117 ++++++++++++++++++ lib/mailtrap/errors.rb | 35 ++++++ lib/mailtrap/mail/base.rb | 4 +- lib/mailtrap/sending.rb | 37 ------ lib/mailtrap/sending/attachment.rb | 52 -------- lib/mailtrap/sending/client.rb | 74 ----------- lib/mailtrap/version.rb | 2 +- mailtrap.gemspec | 4 +- .../sending_is_successful.yml | 0 ...thorization_error_with_array_of_errors.yml | 0 ...ses_sending_error_with_array_of_errors.yml | 0 .../sending_is_successful.yml | 0 .../chooses_host_for_bulk_sending.yml | 37 ++++++ .../chooses_alternative_host.yml | 37 ++++++ .../chooses_host_for_sandbox_sending.yml | 37 ++++++ .../sending_is_successful.yml | 0 ...thorization_error_with_array_of_errors.yml | 0 .../sending_is_successful.yml | 35 ++++++ .../action_mailer/delivery_method_spec.rb | 23 ++-- .../mailtrap/{sending => }/attachment_spec.rb | 4 +- spec/mailtrap/{sending => }/client_spec.rb | 80 ++++++++++-- spec/mailtrap/mail/shared.rb | 2 +- 32 files changed, 565 insertions(+), 280 deletions(-) create mode 100644 examples/action_mailer.rb create mode 100644 examples/email_template.rb create mode 100644 examples/full.rb create mode 100644 lib/mailtrap/attachment.rb create mode 100644 lib/mailtrap/client.rb create mode 100644 lib/mailtrap/errors.rb delete mode 100644 lib/mailtrap/sending.rb delete mode 100644 lib/mailtrap/sending/attachment.rb delete mode 100644 lib/mailtrap/sending/client.rb rename spec/fixtures/vcr_cassettes/{Mailtrap_Sending_Client => Mailtrap_Client}/_send/when_mail/when_all_params_are_set/sending_is_successful.yml (100%) rename spec/fixtures/vcr_cassettes/{Mailtrap_Sending_Client => Mailtrap_Client}/_send/when_mail/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml (100%) rename spec/fixtures/vcr_cassettes/{Mailtrap_Sending_Client => Mailtrap_Client}/_send/when_mail/when_no_subject_and_no_text_set/raises_sending_error_with_array_of_errors.yml (100%) rename spec/fixtures/vcr_cassettes/{Mailtrap_Sending_Client => Mailtrap_Client}/_send/when_mail/with_an_alternative_host/sending_is_successful.yml (100%) create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag/chooses_host_for_bulk_sending.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag_and_alternative_host/chooses_alternative_host.yml create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_sandbox_flag/chooses_host_for_sandbox_sending.yml rename spec/fixtures/vcr_cassettes/{Mailtrap_Sending_Client => Mailtrap_Client}/_send/when_template/when_all_params_are_set/sending_is_successful.yml (100%) rename spec/fixtures/vcr_cassettes/{Mailtrap_Sending_Client => Mailtrap_Client}/_send/when_template/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml (100%) create mode 100644 spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_using_sandbox/sending_is_successful.yml rename spec/mailtrap/{sending => }/attachment_spec.rb (93%) rename spec/mailtrap/{sending => }/client_spec.rb (65%) diff --git a/.rubocop.yml b/.rubocop.yml index d90a2f9..64933f3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,3 +31,11 @@ Style/StringLiterals: Style/StringLiteralsInInterpolation: Enabled: true EnforcedStyle: double_quotes + +Style/FrozenStringLiteralComment: + Exclude: + - 'examples/**/*' + +Style/TrailingCommaInHashLiteral: + Exclude: + - 'examples/**/*' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4faa221..a4b6ce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## [2.0.0] - 2024-03-20 + +- Added arguments for `Mailtrap::Client` + - `bulk` to use Mailtrap bulk sending API + - `sandbox` to use Mailtrap sandbox API + - `inbox_id` required when using Mailtrap sandbox API + +- Removed Sending namespace, affected classes: + - `Mailtrap::Sending::Client` -> `Mailtrap::Client` + - `Mailtrap::Sending::Error` -> `Mailtrap::Error` + - `Mailtrap::Sending::AttachmentContentError` -> `Mailtrap::AttachmentContentError` + - `Mailtrap::Sending::AuthorizationError` -> `Mailtrap::AuthorizationError` + - `Mailtrap::Sending::MailSizeError` -> `Mailtrap::MailSizeError` + - `Mailtrap::Sending::RateLimitError` -> `Mailtrap::RateLimitError` + - `Mailtrap::Sending::RejectionError` -> `Mailtrap::RejectionError` + ## [1.2.2] - 2023-11-01 - Improved error handling diff --git a/Gemfile.lock b/Gemfile.lock index c842a8d..70e78e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - mailtrap (1.2.2) + mailtrap (2.0.0) GEM remote: https://rubygems.org/ diff --git a/README.md b/README.md index 515928d..aef3ab7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ This Ruby gem offers integration with the [official API](https://api-docs.mailtr Quickly add email sending functionality to your Ruby application with Mailtrap. +(This client uses API v2, for v1 refer to [this documentation](https://mailtrap.docs.apiary.io/)) + ## Installation Add this line to your application's Gemfile: @@ -40,97 +42,17 @@ mail = Mailtrap::Mail::Base.new( ) # create client and send -client = Mailtrap::Sending::Client.new(api_key: 'your-api-key') +client = Mailtrap::Client.new(api_key: 'your-api-key') client.send(mail) ``` -### Full +Refer to the [`examples`](examples) folder for other examples. -```ruby -require 'mailtrap' -require 'base64' +- [Full](examples/full.rb) +- [Email template](examples/email_template.rb) +- [ActionMailer](examples/action_mailer.rb) -mail = Mailtrap::Mail::Base.new( - from: { email: 'mailtrap@example.com', name: 'Mailtrap Test' }, - to: [ - { email: 'your@email.com', name: 'Your name' } - ], - cc: [ - { email: 'cc@email.com', name: 'Copy To' } - ], - bcc: [ - { email: 'bcc@email.com', name: 'Hidden Recipient' } - ], - 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 - } -) - -data = File.open('/path/to/image.jpg').read -encoded = Base64.encode64(data).gsub(/\n/,"") - -mail.add_attachment(content: encoded, filename: 'image.png') - -client = Mailtrap::Sending::Client.new(api_key: 'your-api-key') -client.send(mail) -``` - -### Using email template - -```ruby -require 'mailtrap' - -# create mail object -mail = Mailtrap::Mail::FromTemplate.new( - from: { email: 'mailtrap@example.com', name: 'Mailtrap Test' }, - to: [ - { email: 'your@email.com' } - ], - template_uuid: '2f45b0aa-bbed-432f-95e4-e145e1965ba2', - template_variables: { - 'user_name' => 'John Doe' - } -) - -# create client and send -client = Mailtrap::Sending::Client.new(api_key: 'your-api-key') -client.send(mail) -``` - -### ActionMailer - -This gem also adds ActionMailer delivery method. To configure it, add following to your ActionMailer configuration (in Rails projects located in `config/$ENVIRONMENT.rb`): -```ruby -config.action_mailer.delivery_method = :mailtrap -config.action_mailer.mailtrap_settings = { - api_key: ENV.fetch('MAILTRAP_API_KEY') -} -``` -And continue to use ActionMailer as usual. - -To add `category` and `custom_variables`, add them to the mail generation: -```ruby -mail( - to: 'your@email.com', - subject: 'You are awesome!', - category: 'Test category', - custom_variables: { test_variable: 'abc' } -) -``` - -#### Content-Transfer-Encoding +### Content-Transfer-Encoding `mailtrap` gem uses Mailtrap API to send emails. Mailtrap API does not try to replicate SMTP. That is why you should expect some limitations when it comes to @@ -144,6 +66,12 @@ better flexibility in that regard. Go to your _Mailtrap account_ → _Email Send → _Sending Domains_ → _Your domain_ → _SMTP/API Settings_ to find the SMTP configuration example. +## Migration guide v1 → v2 + +Change `Mailtrap::Sending::Client` to `Mailtrap::Client`. + +If you use classes which have `Sending` namespace, remove the namespace like in the example above. + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. diff --git a/examples/action_mailer.rb b/examples/action_mailer.rb new file mode 100644 index 0000000..8d7bada --- /dev/null +++ b/examples/action_mailer.rb @@ -0,0 +1,18 @@ +# This gem adds ActionMailer delivery method. +# To configure it, add following to your ActionMailer configuration +# (in Rails projects located in `config/$ENVIRONMENT.rb`) +config.action_mailer.delivery_method = :mailtrap +config.action_mailer.mailtrap_settings = { + api_key: ENV.fetch('MAILTRAP_API_KEY'), + # bulk: true, # Bulk sending (@see https://help.mailtrap.io/article/113-sending-streams) + # sandbox: true, inbox_id: 12, # Sandbox sending (@see https://help.mailtrap.io/article/109-getting-started-with-mailtrap-email-testing) +} +# And continue to use ActionMailer as usual. + +# To add `category` and `custom_variables`, add them to the mail generation: +mail( + to: 'your@email.com', + subject: 'You are awesome!', + category: 'Test category', + custom_variables: { test_variable: 'abc' } +) diff --git a/examples/email_template.rb b/examples/email_template.rb new file mode 100644 index 0000000..cdb795b --- /dev/null +++ b/examples/email_template.rb @@ -0,0 +1,17 @@ +require 'mailtrap' + +# create mail object +mail = Mailtrap::Mail::FromTemplate.new( + from: { email: 'mailtrap@example.com', name: 'Mailtrap Test' }, + to: [ + { email: 'your@email.com' } + ], + template_uuid: '2f45b0aa-bbed-432f-95e4-e145e1965ba2', + template_variables: { + 'user_name' => 'John Doe' + } +) + +# create client and send +client = Mailtrap::Client.new(api_key: 'your-api-key') +client.send(mail) diff --git a/examples/full.rb b/examples/full.rb new file mode 100644 index 0000000..9a1aac6 --- /dev/null +++ b/examples/full.rb @@ -0,0 +1,48 @@ +require 'mailtrap' +require 'base64' + +mail = Mailtrap::Mail::Base.new( + from: { email: 'mailtrap@example.com', name: 'Mailtrap Test' }, + to: [ + { email: 'your@email.com', name: 'Your name' } + ], + cc: [ + { email: 'cc@email.com', name: 'Copy To' } + ], + bcc: [ + { email: 'bcc@email.com', name: 'Hidden Recipient' } + ], + 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 + } +) + +data = File.read('/path/to/image.jpg') +encoded = Base64.encode64(data).gsub(/\n/, '') + +mail.add_attachment(content: encoded, filename: 'image.png') + +client = Mailtrap::Client.new(api_key: 'your-api-key') + +# Custom host / port +# client = Mailtrap::Client.new(api_key: 'your-api-key', api_host: 'alternative.host.mailtrap.io', api_port: 8080) + +# Bulk sending (@see https://help.mailtrap.io/article/113-sending-streams) +# client = Mailtrap::Client.new(api_key: 'your-api-key', bulk: true) + +# Sandbox sending (@see https://help.mailtrap.io/article/109-getting-started-with-mailtrap-email-testing) +# client = Mailtrap::Client.new(api_key: 'your-api-key', sandbox: true, inbox_id: 12) + +client.send(mail) diff --git a/lib/mailtrap.rb b/lib/mailtrap.rb index bce2b81..238dfc2 100644 --- a/lib/mailtrap.rb +++ b/lib/mailtrap.rb @@ -2,7 +2,7 @@ require_relative 'mailtrap/action_mailer' if defined? ActionMailer require_relative 'mailtrap/mail' -require_relative 'mailtrap/sending' +require_relative 'mailtrap/errors' require_relative 'mailtrap/version' module Mailtrap; end diff --git a/lib/mailtrap/action_mailer/delivery_method.rb b/lib/mailtrap/action_mailer/delivery_method.rb index bee8725..6fa3bd5 100644 --- a/lib/mailtrap/action_mailer/delivery_method.rb +++ b/lib/mailtrap/action_mailer/delivery_method.rb @@ -5,6 +5,8 @@ module ActionMailer class DeliveryMethod attr_accessor :settings + ALLOWED_PARAMS = %i[api_key api_host api_port bulk sandbox inbox_id].freeze + def initialize(settings) self.settings = settings end @@ -18,7 +20,7 @@ def deliver!(message) private def client - @client ||= Mailtrap::Sending::Client.new(**settings.slice(:api_key, :api_host, :api_port)) + @client ||= Mailtrap::Client.new(**settings.slice(*ALLOWED_PARAMS)) end end end diff --git a/lib/mailtrap/attachment.rb b/lib/mailtrap/attachment.rb new file mode 100644 index 0000000..7f1a128 --- /dev/null +++ b/lib/mailtrap/attachment.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'base64' +require 'json' + +module Mailtrap + class Attachment + attr_accessor :type, :filename, :disposition, :content_id + attr_reader :content + + def initialize(content:, filename:, type: nil, disposition: nil, content_id: nil) + self.content = content + @type = type + @filename = filename + @disposition = disposition + @content_id = content_id + end + + def as_json + { + 'content' => content, + 'type' => type, + 'filename' => filename, + 'disposition' => disposition, + 'content_id' => content_id + }.compact + end + + def content=(content) + if content.respond_to?(:read) + @content = encode(content) + else + raise AttachmentContentError unless base64?(content) + + @content = content + end + end + + private + + def encode(io) + string = io.read.encode('UTF-8') unless io.respond_to?(:binmode?) && io.binmode? + Base64.encode64(string).gsub(/\n/, '') + end + + def base64?(string) + string.is_a?(String) && Base64.strict_encode64(Base64.decode64(string)) == string + end + end +end diff --git a/lib/mailtrap/client.rb b/lib/mailtrap/client.rb new file mode 100644 index 0000000..b6f5b37 --- /dev/null +++ b/lib/mailtrap/client.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'json' +require 'net/http' +require 'uri' + +module Mailtrap + class Client + SENDING_API_HOST = 'send.api.mailtrap.io' + BULK_SENDING_API_HOST = 'bulk.api.mailtrap.io' + SANDBOX_API_HOST = 'sandbox.api.mailtrap.io' + API_PORT = 443 + + attr_reader :api_key, :api_host, :api_port, :bulk, :sandbox, :inbox_id + + # Initializes a new Mailtrap::Client instance. + # + # @param [String] api_key The Mailtrap API key to use for sending. Required. + # If not set, is taken from the MAILTRAP_API_KEY environment variable. + # @param [String, nil] api_host The Mailtrap API hostname. If not set, is chosen internally. + # @param [Integer] api_port The Mailtrap API port. Default: 443. + # @param [Boolean] bulk Whether to use the Mailtrap bulk sending API. Default: false. + # If enabled, is incompatible with `sandbox: true`. + # @param [Boolean] sandbox Whether to use the Mailtrap sandbox API. Default: false. + # If enabled, is incompatible with `bulk: true`. + # @param [Integer] inbox_id The sandbox inbox ID to send to. Required if sandbox API is used. + def initialize( # rubocop:disable Metrics/ParameterLists + api_key: ENV.fetch('MAILTRAP_API_KEY'), + api_host: nil, + api_port: API_PORT, + bulk: false, + 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? + + api_host ||= select_api_host(bulk: bulk, sandbox: sandbox) + raise ArgumentError, 'inbox_id is required for sandbox API' if sandbox && inbox_id.nil? + + @api_key = api_key + @api_host = api_host + @api_port = api_port + @bulk = bulk + @sandbox = sandbox + @inbox_id = inbox_id + end + + def send(mail) + raise ArgumentError, 'should be Mailtrap::Mail::Base object' unless mail.is_a? Mail::Base + + request = post_request(request_url, mail.to_json) + response = http_client.request(request) + + handle_response(response) + end + + private + + 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 + BULK_SENDING_API_HOST + else + SENDING_API_HOST + end + end + + def request_url + "/api/send#{sandbox ? "/#{inbox_id}" : ""}" + end + + def http_client + @http_client ||= Net::HTTP.new(api_host, api_port).tap { |client| client.use_ssl = true } + end + + def post_request(path, body) + request = Net::HTTP::Post.new(path) + request.body = body + request['Authorization'] = "Bearer #{api_key}" + request['Content-Type'] = 'application/json' + request['User-Agent'] = 'mailtrap-ruby (https://github.com/railsware/mailtrap-ruby)' + + request + end + + def handle_response(response) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength + case response + when Net::HTTPOK + json_response(response.body) + when Net::HTTPBadRequest + raise Mailtrap::Error, json_response(response.body)[:errors] + when Net::HTTPUnauthorized + raise Mailtrap::AuthorizationError, json_response(response.body)[:errors] + when Net::HTTPForbidden + raise Mailtrap::RejectionError, json_response(response.body)[:errors] + when Net::HTTPPayloadTooLarge + raise Mailtrap::MailSizeError, ['message too large'] + when Net::HTTPTooManyRequests + raise Mailtrap::RateLimitError, ['too many requests'] + when Net::HTTPClientError + raise Mailtrap::Error, ['client error'] + when Net::HTTPServerError + raise Mailtrap::Error, ['server error'] + else + raise Mailtrap::Error, ["unexpected status code=#{response.code}"] + end + end + + def json_response(body) + JSON.parse(body, symbolize_names: true) + end + end +end diff --git a/lib/mailtrap/errors.rb b/lib/mailtrap/errors.rb new file mode 100644 index 0000000..6c7600d --- /dev/null +++ b/lib/mailtrap/errors.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require_relative 'attachment' +require_relative 'client' + +module Mailtrap + class AttachmentContentError < StandardError; end + + class Error < StandardError + attr_reader :messages + + def initialize(messages) + @messages = messages + + super(messages.join(', ')) + end + end + + # AuthorizationError is raised when invalid token is used. + class AuthorizationError < Error; end + + # MailSizeError is raised when mail is too large. + class MailSizeError < Error; end + + # RateLimitError is raised when client performing too many requests. + class RateLimitError < Error; end + + # RejectionError is raised when server refuses to process the request. Use + # error message to debug the problem. + # + # *Some* possible reasons: + # * Account is banned + # * Domain is not verified + class RejectionError < Error; end +end diff --git a/lib/mailtrap/mail/base.rb b/lib/mailtrap/mail/base.rb index f573a6d..cc722be 100644 --- a/lib/mailtrap/mail/base.rb +++ b/lib/mailtrap/mail/base.rb @@ -59,11 +59,11 @@ def to_json(*args) end def attachments=(attachments) - @attachments = attachments.map { |attachment| Mailtrap::Sending::Attachment.new(**attachment) } + @attachments = attachments.map { |attachment| Mailtrap::Attachment.new(**attachment) } end def add_attachment(content:, filename:, type: nil, disposition: nil, content_id: nil) - attachment = Mailtrap::Sending::Attachment.new( + attachment = Mailtrap::Attachment.new( content: content, filename: filename, type: type, diff --git a/lib/mailtrap/sending.rb b/lib/mailtrap/sending.rb deleted file mode 100644 index 2c9a2dc..0000000 --- a/lib/mailtrap/sending.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -require_relative 'sending/attachment' -require_relative 'sending/client' - -module Mailtrap - module Sending - class AttachmentContentError < StandardError; end - - class Error < StandardError - attr_reader :messages - - def initialize(messages) - @messages = messages - - super(messages.join(', ')) - end - end - - # AuthorizationError is raised when invalid token is used. - class AuthorizationError < Error; end - - # MailSizeError is raised when mail is too large. - class MailSizeError < Error; end - - # RateLimitError is raised when client performing too many requests. - class RateLimitError < Error; end - - # RejectionError is raised when server refuses to process the request. Use - # error message to debug the problem. - # - # *Some* possible reasons: - # * Account is banned - # * Domain is not verified - class RejectionError < Error; end - end -end diff --git a/lib/mailtrap/sending/attachment.rb b/lib/mailtrap/sending/attachment.rb deleted file mode 100644 index 8c0cf53..0000000 --- a/lib/mailtrap/sending/attachment.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -require 'base64' -require 'json' - -module Mailtrap - module Sending - class Attachment - attr_accessor :type, :filename, :disposition, :content_id - attr_reader :content - - def initialize(content:, filename:, type: nil, disposition: nil, content_id: nil) - self.content = content - @type = type - @filename = filename - @disposition = disposition - @content_id = content_id - end - - def as_json - { - 'content' => content, - 'type' => type, - 'filename' => filename, - 'disposition' => disposition, - 'content_id' => content_id - }.compact - end - - def content=(content) - if content.respond_to?(:read) - @content = encode(content) - else - raise AttachmentContentError unless base64?(content) - - @content = content - end - end - - private - - def encode(io) - string = io.read.encode('UTF-8') unless io.respond_to?(:binmode?) && io.binmode? - Base64.encode64(string).gsub(/\n/, '') - end - - def base64?(string) - string.is_a?(String) && Base64.strict_encode64(Base64.decode64(string)) == string - end - end - end -end diff --git a/lib/mailtrap/sending/client.rb b/lib/mailtrap/sending/client.rb deleted file mode 100644 index 5ef123e..0000000 --- a/lib/mailtrap/sending/client.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -require 'json' -require 'net/http' -require 'uri' - -module Mailtrap - module Sending - class Client - API_HOST = 'send.api.mailtrap.io' - API_PORT = 443 - - attr_reader :api_key, :api_host, :api_port - - def initialize(api_key: ENV.fetch('MAILTRAP_API_KEY'), api_host: API_HOST, api_port: API_PORT) - @api_key = api_key - @api_host = api_host - @api_port = api_port - end - - def send(mail) - raise ArgumentError, 'should be Mailtrap::Mail::Base object' unless mail.is_a? Mail::Base - - request = post_request('/api/send', mail.to_json) - response = http_client.request(request) - - handle_response(response) - end - - private - - def http_client - @http_client ||= Net::HTTP.new(api_host, api_port).tap { |client| client.use_ssl = true } - end - - def post_request(path, body) - request = Net::HTTP::Post.new(path) - request.body = body - request['Authorization'] = "Bearer #{api_key}" - request['Content-Type'] = 'application/json' - request['User-Agent'] = 'mailtrap-ruby (https://github.com/railsware/mailtrap-ruby)' - - request - end - - def handle_response(response) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength - case response - when Net::HTTPOK - json_response(response.body) - when Net::HTTPBadRequest - raise Mailtrap::Sending::Error, json_response(response.body)[:errors] - when Net::HTTPUnauthorized - raise Mailtrap::Sending::AuthorizationError, json_response(response.body)[:errors] - when Net::HTTPForbidden - raise Mailtrap::Sending::RejectionError, json_response(response.body)[:errors] - when Net::HTTPPayloadTooLarge - raise Mailtrap::Sending::MailSizeError, ['message too large'] - when Net::HTTPTooManyRequests - raise Mailtrap::Sending::RateLimitError, ['too many requests'] - when Net::HTTPClientError - raise Mailtrap::Sending::Error, ['client error'] - when Net::HTTPServerError - raise Mailtrap::Sending::Error, ['server error'] - else - raise Mailtrap::Sending::Error, ["unexpected status code=#{response.code}"] - end - end - - def json_response(body) - JSON.parse(body, symbolize_names: true) - end - end - end -end diff --git a/lib/mailtrap/version.rb b/lib/mailtrap/version.rb index 273e5ca..7f38167 100644 --- a/lib/mailtrap/version.rb +++ b/lib/mailtrap/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Mailtrap - VERSION = '1.2.2' + VERSION = '2.0.0' end diff --git a/mailtrap.gemspec b/mailtrap.gemspec index 2f4f3aa..5b39a27 100644 --- a/mailtrap.gemspec +++ b/mailtrap.gemspec @@ -23,7 +23,9 @@ Gem::Specification.new do |spec| # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject do |f| - (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|github|travis|circleci)|appveyor)}) + (f == __FILE__) || f.match( + %r{\A(?:(?:bin|test|spec|features|examples)/|\.(?:git|github|travis|circleci)|appveyor)} + ) end end spec.require_paths = ['lib'] diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/when_all_params_are_set/sending_is_successful.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/when_all_params_are_set/sending_is_successful.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/when_all_params_are_set/sending_is_successful.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/when_all_params_are_set/sending_is_successful.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/when_no_subject_and_no_text_set/raises_sending_error_with_array_of_errors.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/when_no_subject_and_no_text_set/raises_sending_error_with_array_of_errors.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/when_no_subject_and_no_text_set/raises_sending_error_with_array_of_errors.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/when_no_subject_and_no_text_set/raises_sending_error_with_array_of_errors.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/with_an_alternative_host/sending_is_successful.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_an_alternative_host/sending_is_successful.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_mail/with_an_alternative_host/sending_is_successful.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_an_alternative_host/sending_is_successful.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag/chooses_host_for_bulk_sending.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag/chooses_host_for_bulk_sending.yml new file mode 100644 index 0000000..39d7510 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag/chooses_host_for_bulk_sending.yml @@ -0,0 +1,37 @@ +--- +http_interactions: +- request: + method: post + uri: https://bulk.api.mailtrap.io/api/send + body: + encoding: UTF-8 + string: '{"from":{"email":"mailtrap@mailtrap.io","name":"Mailtrap Test"},"to":[{"email":"mailtrap@railsware.com"}],"cc":[],"bcc":[],"subject":"You are awesome!","text":"Congrats for sending test email with Mailtrap!","attachments":[{"content":"aGVsbG8gd29ybGQ=","filename":"attachment.txt"}],"headers":{},"custom_variables":{},"category":"Integration Test"}' + 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: + - Tue, 19 Mar 2024 03:26:55 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '16' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"success":true}' + recorded_at: Tue, 19 Mar 2024 03:26:55 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag_and_alternative_host/chooses_alternative_host.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag_and_alternative_host/chooses_alternative_host.yml new file mode 100644 index 0000000..c2d4360 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_bulk_flag_and_alternative_host/chooses_alternative_host.yml @@ -0,0 +1,37 @@ +--- +http_interactions: +- request: + method: post + uri: https://alternative.host.mailtrap.io:8080/api/send + body: + encoding: UTF-8 + string: '{"from":{"email":"mailtrap@mailtrap.io","name":"Mailtrap Test"},"to":[{"email":"mailtrap@railsware.com"}],"cc":[],"bcc":[],"subject":"You are awesome!","text":"Congrats for sending test email with Mailtrap!","attachments":[{"content":"aGVsbG8gd29ybGQ=","filename":"attachment.txt"}],"headers":{},"custom_variables":{},"category":"Integration Test"}' + 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: + - Thu, 13 Oct 2022 22:08:10 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '16' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"success":true}' + recorded_at: Thu, 13 Oct 2022 22:08:10 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_sandbox_flag/chooses_host_for_sandbox_sending.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_sandbox_flag/chooses_host_for_sandbox_sending.yml new file mode 100644 index 0000000..86ea04f --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_mail/with_sandbox_flag/chooses_host_for_sandbox_sending.yml @@ -0,0 +1,37 @@ +--- +http_interactions: +- request: + method: post + uri: https://sandbox.api.mailtrap.io:443/api/send/12 + body: + encoding: UTF-8 + string: '{"from":{"email":"mailtrap@mailtrap.io","name":"Mailtrap Test"},"to":[{"email":"mailtrap@railsware.com"}],"cc":[],"bcc":[],"subject":"You are awesome!","text":"Congrats for sending test email with Mailtrap!","attachments":[{"content":"aGVsbG8gd29ybGQ=","filename":"attachment.txt"}],"headers":{},"custom_variables":{},"category":"Integration Test"}' + 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: + - Thu, 13 Oct 2022 22:08:10 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '16' + Connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"success":true}' + recorded_at: Thu, 13 Oct 2022 22:08:10 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_template/when_all_params_are_set/sending_is_successful.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_all_params_are_set/sending_is_successful.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_template/when_all_params_are_set/sending_is_successful.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_all_params_are_set/sending_is_successful.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_template/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml similarity index 100% rename from spec/fixtures/vcr_cassettes/Mailtrap_Sending_Client/_send/when_template/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml rename to spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_api_key_is_incorrect/raises_authorization_error_with_array_of_errors.yml diff --git a/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_using_sandbox/sending_is_successful.yml b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_using_sandbox/sending_is_successful.yml new file mode 100644 index 0000000..51967e0 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/Mailtrap_Client/_send/when_template/when_using_sandbox/sending_is_successful.yml @@ -0,0 +1,35 @@ +--- +http_interactions: + - request: + method: post + uri: https://sandbox.api.mailtrap.io/api/send/13 + body: + encoding: UTF-8 + string: '{"from":{"email":"mailtrap@mailtrap.io","name":"Mailtrap Test"},"to":[{"email":"mailtrap@railsware.com"}],"cc":[],"bcc":[],"attachments":[{"content":"aGVsbG8gd29ybGQ=","filename":"attachment.txt"}],"headers":{},"custom_variables":{},"template_uuid":"aeb1ec59-2737-4a1d-9c95-0baf3be49d74","template_variables":{"user_name":"John Doe"}}' + 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: + Content-Type: + - application/json; charset=utf-8 + Date: + - Tue, 13 Dec 2022 21:23:26 GMT + Content-Length: + - '71' + body: + encoding: UTF-8 + string: '{"success":true,"message_ids":["617103b5-7b2c-11ed-b344-0242ac1c0107"]}' + recorded_at: Tue, 13 Dec 2022 21:23:26 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/mailtrap/action_mailer/delivery_method_spec.rb b/spec/mailtrap/action_mailer/delivery_method_spec.rb index 352f342..ce28d7c 100644 --- a/spec/mailtrap/action_mailer/delivery_method_spec.rb +++ b/spec/mailtrap/action_mailer/delivery_method_spec.rb @@ -7,7 +7,18 @@ subject(:deliver!) { described_class.new(settings).deliver!(message) } let(:settings) { { api_key: 'correct-api-key' } } - let(:message) { Mail::Message.new(params) } + let(:message) do + Mail::Message.new(params).tap do |message| + message.text_part = 'Some text' + message.html_part = '
HTML part
' + message.headers('X-Special-Domain-Specific-Header': 'SecretValue') + message.headers('One-more-custom-header': 'CustomValue') + message.attachments['file.txt'] = File.read('spec/fixtures/files/attachments/file.txt') + message.attachments['file.txt'].content_id = '' + message.attachments.inline['file.png'] = File.read('spec/fixtures/files/attachments/file.png') + message.attachments['file.png'].content_id = '' + end + end let(:params) do { from: 'Mailtrap Test ', @@ -32,18 +43,12 @@ before do allow(::Mail::ContentTypeField).to receive(:generate_boundary).and_return('--==_mimepart_random_boundary') - message.text_part = 'Some text' - message.html_part = '
HTML part
' - message.headers('X-Special-Domain-Specific-Header': 'SecretValue') - message.headers('One-more-custom-header': 'CustomValue') - message.attachments['file.txt'] = File.read('spec/fixtures/files/attachments/file.txt') - message.attachments['file.txt'].content_id = '' - message.attachments.inline['file.png'] = File.read('spec/fixtures/files/attachments/file.png') - message.attachments['file.png'].content_id = '' + allow(Mailtrap::Client).to receive(:new).and_call_original end it 'converts the message and sends via API' do expect(deliver!).to eq({ success: true, message_ids: expected_message_ids }) + expect(Mailtrap::Client).to have_received(:new).with(api_key: 'correct-api-key') end end end diff --git a/spec/mailtrap/sending/attachment_spec.rb b/spec/mailtrap/attachment_spec.rb similarity index 93% rename from spec/mailtrap/sending/attachment_spec.rb rename to spec/mailtrap/attachment_spec.rb index 09f7e89..eb5659a 100644 --- a/spec/mailtrap/sending/attachment_spec.rb +++ b/spec/mailtrap/attachment_spec.rb @@ -3,7 +3,7 @@ require 'stringio' # rubocop:disable RSpec/MultipleMemoizedHelpers -RSpec.describe Mailtrap::Sending::Attachment do +RSpec.describe Mailtrap::Attachment do subject(:attachment) do described_class.new( content: content, @@ -43,7 +43,7 @@ let(:content) { 'non-base64' } it 'raises AttachmentContentError' do - expect { attachment }.to raise_error(Mailtrap::Sending::AttachmentContentError) + expect { attachment }.to raise_error(Mailtrap::AttachmentContentError) end end end diff --git a/spec/mailtrap/sending/client_spec.rb b/spec/mailtrap/client_spec.rb similarity index 65% rename from spec/mailtrap/sending/client_spec.rb rename to spec/mailtrap/client_spec.rb index e5118c2..5c52f8f 100644 --- a/spec/mailtrap/sending/client_spec.rb +++ b/spec/mailtrap/client_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe Mailtrap::Sending::Client do +RSpec.describe Mailtrap::Client do subject(:client) { described_class.new(api_key: api_key) } let(:api_key) { 'correct-api-key' } @@ -38,7 +38,7 @@ it 'raises sending error with array of errors' do expect { send }.to raise_error do |error| - expect(error).to be_a(Mailtrap::Sending::Error) + expect(error).to be_a(Mailtrap::Error) expect(error.message).to eq("'subject' is required, must specify either text or html body") expect(error.messages).to eq(["'subject' is required", 'must specify either text or html body']) end @@ -50,7 +50,7 @@ it 'raises authorization error with array of errors' do expect { send }.to raise_error do |error| - expect(error).to be_a(Mailtrap::Sending::AuthorizationError) + expect(error).to be_a(Mailtrap::AuthorizationError) expect(error.message).to eq('Unauthorized') expect(error.messages).to eq(['Unauthorized']) end @@ -72,6 +72,52 @@ expect(send).to eq({ message_ids: ['867394cd-4b43-11ed-af38-0a58a9feac02'], success: true }) end end + + context 'with bulk flag' do + let(:client) do + described_class.new(api_key: api_key, bulk: true) + end + + it 'chooses host for bulk sending' do + expect(send).to eq({ success: true }) + end + end + + context 'with bulk flag and alternative host' do + let(:client) do + described_class.new(api_key: api_key, bulk: true, api_host: 'alternative.host.mailtrap.io', api_port: 8080) + end + + it 'chooses alternative host' do + expect(send).to eq({ success: true }) + end + end + + context 'with sandbox flag' do + let(:client) do + described_class.new(api_key: api_key, sandbox: true, inbox_id: 12) + end + + it 'chooses host for sandbox sending' do + expect(send).to eq({ success: true }) + end + end + + context 'with sandbox flag without inbox id' do + let(:client) do + described_class.new(api_key: api_key, sandbox: true) + end + + it { expect { send }.to raise_error(ArgumentError, 'inbox_id is required for sandbox API') } + end + + context 'with bulk and sandbox flag' do + let(:client) do + described_class.new(api_key: api_key, bulk: true, sandbox: true) + end + + it { expect { send }.to raise_error(ArgumentError, 'bulk mode is not applicable for sandbox API') } + end end context 'when template' do @@ -100,12 +146,22 @@ it 'raises authorization error with array of errors' do expect { send }.to raise_error do |error| - expect(error).to be_a(Mailtrap::Sending::AuthorizationError) + expect(error).to be_a(Mailtrap::AuthorizationError) expect(error.message).to eq('Unauthorized') expect(error.messages).to eq(['Unauthorized']) end end end + + context 'when using sandbox' do + let(:client) do + described_class.new(api_key: api_key, sandbox: true, inbox_id: 13) + end + + it 'sending is successful' do + expect(send).to eq({ message_ids: ['617103b5-7b2c-11ed-b344-0242ac1c0107'], success: true }) + end + end end end @@ -129,49 +185,49 @@ def stub_api_send(status, body = nil) it 'handles 400' do stub_api_send 400, '{"errors":["error"]}' do - expect { send_mail }.to raise_error(Mailtrap::Sending::Error) + expect { send_mail }.to raise_error(Mailtrap::Error) end end it 'handles 401' do stub_api_send 401, '{"errors":["Unauthorized"]}' do - expect { send_mail }.to raise_error(Mailtrap::Sending::AuthorizationError) + expect { send_mail }.to raise_error(Mailtrap::AuthorizationError) end end it 'handles 403' do stub_api_send 403, '{"errors":["Account is banned"]}' do - expect { send_mail }.to raise_error(Mailtrap::Sending::RejectionError) + expect { send_mail }.to raise_error(Mailtrap::RejectionError) end end it 'handles 413' do stub_api_send 413 do - expect { send_mail }.to raise_error(Mailtrap::Sending::MailSizeError) + expect { send_mail }.to raise_error(Mailtrap::MailSizeError) end end it 'handles 429' do stub_api_send 429 do - expect { send_mail }.to raise_error(Mailtrap::Sending::RateLimitError) + expect { send_mail }.to raise_error(Mailtrap::RateLimitError) end end it 'handles generic client errors' do stub_api_send 418, '🫖' do - expect { send_mail }.to raise_error(Mailtrap::Sending::Error, 'client error') + expect { send_mail }.to raise_error(Mailtrap::Error, 'client error') end end it 'handles server errors' do stub_api_send 504, '🫖' do - expect { send_mail }.to raise_error(Mailtrap::Sending::Error, 'server error') + expect { send_mail }.to raise_error(Mailtrap::Error, 'server error') end end it 'handles unexpected response status code' do stub_api_send 307 do - expect { send_mail }.to raise_error(Mailtrap::Sending::Error, 'unexpected status code=307') + expect { send_mail }.to raise_error(Mailtrap::Error, 'unexpected status code=307') end end end diff --git a/spec/mailtrap/mail/shared.rb b/spec/mailtrap/mail/shared.rb index edf66b3..3ce2916 100644 --- a/spec/mailtrap/mail/shared.rb +++ b/spec/mailtrap/mail/shared.rb @@ -11,7 +11,7 @@ describe 'attachment_params' do subject(:attachment) { attachments_list.first } - it { is_expected.to be_a(Mailtrap::Sending::Attachment) } + it { is_expected.to be_a(Mailtrap::Attachment) } its(:content) { is_expected.to eq('aGVsbG8gd29ybGQ=') } its(:filename) { is_expected.to eq('attachment.txt') } end