Rails
Send your Rails app's mail through Wakio with Action Mailer. Your mailers stay as they are; you change configuration only.
Install
Add the gem to your Gemfile and run bundle install.
gem "wakio"
Configure Action Mailer
Set the delivery method and its settings in the environment that should send through Wakio, usually config/environments/production.rb.
config.action_mailer.delivery_method = :wakio
config.action_mailer.wakio_settings = {
api_key: Rails.application.credentials.wakio_api_key,
host: "https://api.wakio.net"
}
api_keyis a Wakio API key, sent as a bearer token.hostis the bare origin, and it must behttps: the gem refuses anhttphost other thanlocalhostrather than send your key in cleartext. Do not add a path:https://api.wakio.net/apiresolves tohttps://api.wakio.net/v1/emails, and the/apiis dropped.open_timeoutandread_timeoutare optional, in seconds, and default to 5 and 10.
Every existing mailer keeps working unchanged. Its subject, its HTML and text parts, and its to, cc, bcc, reply_to and from go to Wakio as they are, and Wakio sends them without adding a footer or tracking. Recipients keep their display names, so email_address_with_name(user.email, user.name) shows the name in the To line; reply_to is sent as bare addresses. A mailer with only a text view sends a text-only email.
The from address
The domain of from must be one of your sendable domains, matched exactly. [email protected] needs example.com itself verified; a verified mail.example.com does not cover it. A mismatch raises with 422 no_verified_sending_domain.
A mailer with no from goes out from no-reply@ one of your sendable domains. With more than one, set default from: in ApplicationMailer to choose. A new Rails app's ApplicationMailer sets default from: "[email protected]"; change it before your first send.
Handle a suppressed address
Wakio suppresses addresses that have hard-bounced or complained. A send to one is accepted rather than refused, and the response says which recipients were suppressed.
When every recipient of a message was suppressed, deliver! raises Wakio::Suppressed, so a one-recipient mailer never reports success for a message nobody received. A retry cannot help, so handle it in the mailer instead of letting your job backend retry it. rescue_from also catches it under deliver_later:
class ApplicationMailer < ActionMailer::Base
rescue_from Wakio::Suppressed do |error|
Rails.logger.info(error.message)
end
end
When only some recipients were suppressed, deliver! returns normally.
Handle a refusal
A refusal from Wakio is a Wakio::DeliveryError carrying status, code (the response's error, such as "invalid_request") and the parsed body. code is empty when the response is not JSON or names no error, so fall back to status when you log it. Which class it is tells you whether retrying the same message can help.
| Class | Raised for | What to do |
|---|---|---|
Wakio::Rejected | 400, 413, 422 invalid_request, 422 idempotency_key_reused | Retrying this message cannot help. Report it and drop it. invalid_request usually means a mailer bug, such as an unquoted comma in a from display name, and detail names the field. idempotency_key_reused means an earlier request with that key already went out. |
Wakio::Misconfigured | 401 invalid_api_key, 402 not_entitled, 403 account_suspended, 404, 422 no_verified_sending_domain, 422 unknown_template, a redirect, an http host, and any other 4xx | Your account or configuration needs fixing, and the same message sends once it is. Retry it. |
Wakio::DeliveryError | 409 request_in_flight, every 5xx, and a success response the gem cannot read | Retry it. After an unreadable success response Wakio may already have accepted the send, so only a send with an idempotency key is safe to repeat. |
Wakio::Misconfigured covers every 4xx the gem does not recognize, so a new error code is retried rather than dropped. Network failures are not wrapped: a timeout raises Net::OpenTimeout or Net::ReadTimeout, and a refused connection or TLS error raises Ruby's own error. Retry those too. A read timeout can come after Wakio accepted the send, so a retry without an idempotency key may send twice.
Retry the mail that should be retried
Action Mailer's delivery job retries nothing by itself. Whether a failed deliver_later runs again depends on your job backend: Sidekiq retries a failed job by default, but Solid Queue, the default in Rails 8, and GoodJob do not. Give the delivery job its own retry policy so the answer does not depend on the backend:
class WakioDeliveryJob < ActionMailer::MailDeliveryJob
retry_on Wakio::DeliveryError, Net::OpenTimeout, Net::ReadTimeout, wait: :polynomially_longer, attempts: 25
end
config.action_mailer.delivery_job = "WakioDeliveryJob"
On Sidekiq, skip that job. Sidekiq already retries every failed job, about 25 times over three weeks, and a job that gives up under retry_on raises into Sidekiq's own retries on top. What Sidekiq needs is the opposite: a way to stop the messages no retry can send.
Then handle the two errors no retry can fix in the mailer, where they are caught before the job sees them:
class ApplicationMailer < ActionMailer::Base
rescue_from Wakio::Suppressed do |error|
Rails.logger.info(error.message)
end
rescue_from Wakio::Rejected do |error|
Rails.error.report(error)
end
end
With both in place, a password reset sent while your API key was revoked goes out once you fix the key, within the retry window of about three weeks.
Rails.error.report reaches only an error tracker subscribed to Rails' error reporter, and not every tracker subscribes by default: Rollbar, for one, needs enable_rails_error_subscriber. If yours does not, call it directly instead, such as Rollbar.error(error).
deliver_now has no job to retry it, so any of these errors raises into the code that called it. Devise sends with deliver_now from its own mailer, which does not inherit from your ApplicationMailer unless you set Devise.parent_mailer = "ApplicationMailer".
Limits
- A message names at most
50recipients.to,ccandbccare counted together once duplicate addresses collapse, and a message over the limit is refused, not truncated. - The gem does not send the mailer's
attachments. For a send that needs them, call the API directly. - Every recipient gets their own message and delivery record. A
cc'd address receives a separate message, so reply-all does not reach them.
Make a send idempotent
Set a header on the message and it becomes the request's Idempotency-Key.
class OrderMailer < ApplicationMailer
def receipt(order)
headers["X-Wakio-Idempotency-Key"] = "order-#{order.id}-receipt"
mail(to: order.email)
end
end
A repeated request that carries a key Wakio has already seen, with a byte-identical body, returns the original response instead of sending again, for 24 hours. Supply a key when a duplicate would be worse than a missing send, such as receipts and welcome emails. Leave it off, or vary it, when a repeat is the point, such as a resent one-time code. The gem never invents a key, because only you know whether two identical requests are a retry or a deliberate repeat.
The same key with a different body raises Wakio::Rejected with idempotency_key_reused. A retry renders the mailer again, so a keyed mailer must produce the same bytes every time: keep expiring tokens, timestamps and live counts out of it.
For the HTTP request behind all of this, see Send an email.