Action Mailer delivery method for Duva, the transactional email API hosted in Canada. Your mailers,
deliver_later jobs and Devise emails go out through Duva with no change to your mailer code.
Requires Ruby 3.3+ and Rails (Action Mailer) 7.2 to 8.x. Built on the official
duva-mail library.
# Gemfile
gem "duva-rails"You also need a Duva API key and a verified domain (documentation).
# config/environments/production.rb
config.action_mailer.delivery_method = :duva
config.action_mailer.duva_settings = {
api_key: Rails.application.credentials.dig(:duva, :api_key), # or ENV["DUVA_API_KEY"]
domain: "example.com" # or ENV["DUVA_DOMAIN"]
}
config.action_mailer.raise_delivery_errors = true# app/mailers/application_mailer.rb
default from: "Example <notifications@example.com>" # the address must belong to the Duva domainOptional settings: base_url (default https://api.duva.ca), timeout (seconds, default 10), max_retries (default 2).
api_key and domain fall back to the DUVA_API_KEY and DUVA_DOMAIN environment variables.
| Duva | |
|---|---|
| From, To, Cc, Bcc, Reply-To | from, to, cc, bcc, reply_to (an address only in smtp_envelope_to is sent as Bcc) |
text and html parts, or a single-part body |
text, html |
attachments[...], attachments.inline[...] |
attachments (inline ones keep their content_id) |
List-Unsubscribe, List-Unsubscribe-Post, List-Id, In-Reply-To, References, Auto-Submitted, Precedence, Importance, Feedback-ID, and X-* headers |
headers |
X-Duva-Tags, X-Duva-Metadata, X-Duva-Track, X-Idempotency-Key |
read by this gem, never sent (see below) |
Duva-specific options are set as headers in the mailer:
def welcome(user)
headers["X-Duva-Tags"] = "welcome, onboarding" # tags
headers["X-Duva-Metadata"] = { user_id: user.id }.to_json # metadata (JSON object)
headers["X-Duva-Track"] = "opens, clicks" # tracking (needs an HTML part)
headers["X-Idempotency-Key"] = "welcome-#{user.id}" # see "Idempotency"
mail(to: user.email, subject: "Welcome")
endNotes:
- Other headers are not forwarded (Duva rejects them), nor are
X-Duva*,X-Kumo*,X-Tenant*andX-Campaign*. - A display name Duva refuses (it contains
@, quotes,<>, control characters or an encoded word=?…?=) is dropped; the address is kept. - Delivery is asynchronous: a successful
deliver_nowmeans Duva accepted the message, not that it was delivered. Use Duva's webhooks or events for the outcome. - A Duva error (
Duva::Errorand its subclasses, from theduva-maillibrary) is raised fromdeliver_now, and retried by Active Job indeliver_lateraccording to your job configuration.
A retried deliver_later job builds a new message, so a retry after a network timeout could send twice. Set a stable key
for it (an order ID, for example) with the X-Idempotency-Key header. Duva ignores a second message with the same key.
Use ActionMailer::Base.delivery_method = :test in your tests as usual. To exercise this gem without a network, give it
the fake transport of the client library:
transport = Duva::TestTransport.new { |_config, _spec| Duva::TestTransport.response("id" => "msg_...", "status" => "queued") }
ActionMailer::Base.duva_settings = { api_key: "dv_test", domain: "example.com", transport: transport }
# ... deliver ...
transport.requests.last.body # what would have been sentMIT.