Send email from Ruby on Rails
A twenty-line Action Mailer delivery method sends every mailer through Pigeon Atlas over HTTP. Your mailers, views and specs do not change; the SMTP settings go away.
Updated September 15, 2026
Action Mailer already separates what a mailer says from how it is sent, and "how" is a delivery method — an object with a deliver!(mail). Rails ships :smtp, :sendmail and :test. Adding :pigeon_atlas is twenty lines, and afterwards every mailer in the application, every deliver_later, every mailer spec, works as before. It is how Pigeon Atlas sends its own mail.
The delivery method
# app/mailers/pigeon_atlas_delivery.rb
require "net/http"
class PigeonAtlasDelivery
ENDPOINT = URI("https://pigeonatlas.com/v1/emails")
def initialize(settings = {})
@api_key = settings.fetch(:api_key) { ENV.fetch("PIGEONATLAS_API_KEY") }
end
def deliver!(mail)
request = Net::HTTP::Post.new(ENDPOINT, "Content-Type" => "application/json",
"Authorization" => "Bearer #{@api_key}",
"Idempotency-Key" => mail.message_id.to_s)
request.body = {
from: mail[:from].to_s,
to: mail.to,
subject: mail.subject,
html: mail.html_part&.decoded,
text: mail.text_part&.decoded || (mail.body.decoded unless mail.multipart?),
reply_to: mail.reply_to&.first
}.compact.to_json
response = Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: true, read_timeout: 15) { |http| http.request(request) }
raise "Pigeon Atlas refused the message: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
mail
end
end
Register it and select it:
# config/initializers/pigeon_atlas.rb
ActionMailer::Base.add_delivery_method :pigeon_atlas, PigeonAtlasDelivery
# config/environments/production.rb
config.action_mailer.delivery_method = :pigeon_atlas
config.action_mailer.default_url_options = { host: "yourapp.com", protocol: "https" }
Development keeps its default and test keeps :test, so ActionMailer::Base.deliveries still fills in specs and nothing sends from a laptop. The Idempotency-Key is the Message-ID Rails already generated, which means a deliver_later job that is retried after a timeout returns the original message rather than sending a second.
from has to be a verified domain
default from: in ApplicationMailer must be an address on a domain you added and verified in Pigeon Atlas; anything else is refused with 403. Keep it in one place:
class ApplicationMailer < ActionMailer::Base
default from: "Your App <hello@mail.yourapp.com>", reply_to: "support@yourapp.com"
layout "mailer"
end
What you gain over SMTP
Delivery events. Every message the delivery method sends is a row on the messages page with what happened to it — delivered, bounced, complained — and the delivery webhook can post those events back into your application, which Net::SMTP could never tell you.
Before it works
Add mail.yourapp.com in Pigeon Atlas, publish the DKIM, SPF and MX records it shows, and wait for the three ticks. Put the key in config/credentials or the environment — never in the initializer — and use a subdomain rather than the bare domain, so password resets and any newsletter you later send keep separate reputations.
A thousand a month free, no card
Enough to verify a domain and run a small app from it.