A Snails app builds the message; your app owns the way it leaves the process. There are no templates and no built-in SMTP client — you write the subject and body in code and register a delivery class that talks to your provider.
Create a mailer#
Put mailer classes in app/mailers/. A small app needs one base class and one mailer:
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "hello@example.com"
end# app/mailers/welcome_mailer.rb
class WelcomeMailer < ApplicationMailer
def welcome(email)
mail(
to: email,
subject: "Welcome",
body: "Thanks for joining us."
)
end
def self.welcome(email)
new.welcome(email)
end
enddefault sets header values for every message built by that class and its subclasses. A value passed to mail wins over the default; a subclass can add its own default. Only to, from, replyto, subject, and body are modeled. body is the message text — there is no template lookup.
Mailers under app/mailers/ load through gen/mailers.rb; drop the file there (subfolders included) and the next bin/dev or bin/test adds it to the list.
Register a delivery class#
The app sends through a class you register in config/environment.rb. The class answers deliver(message) and returns the message:
# app/lib/my_mail_delivery.rb
class MyMailDelivery
def self.deliver(message)
# Send message.to, message.from, message.subject, and message.body
# through your provider's HTTP API.
message
end
end# config/environment.rb
ActionMailer::Base.delivery_method = MyMailDeliveryThe class is read at delivery time, so you can swap it per environment. A fresh app registers ActionMailer::DeliveryLog in test mode; keep that line last so tests keep the capture. With no class registered in development or production, the first delivery raises instead of silently going nowhere.
Send now or later#
WelcomeMailer.welcome("sue@example.com").deliver_now
WelcomeMailer.welcome("sue@example.com").deliver_laterdeliver_now calls the delivery class on the spot and returns the message. deliver_later hands the message to the job queue and returns without waiting for the provider. In test mode it runs inline so the capture sees it immediately; in a process with no worker loop it also runs inline rather than stranding the message.
A failure from a later delivery is recorded by the job system rather than raised in the caller. If you need to handle a provider failure directly, use deliver_now.
Test it#
In test mode, a fresh app records each delivery to tmp/mail-capture.jsonl — recipient, sender, and subject; not the body, which may carry a reset token. A harness case can count and read deliveries from the running server:
assert_emails 1 do
post("/signup", params: { email: "sue@example.com" }, as: :json)
end
assert_equal "Welcome", emails.last["subject"]assert_emails counts the lines the block added; emails returns the run's rows in send order, each with "to" (an Array), "from", and "subject". The gate empties the file before boot, so a case never sees another run's mail.
For a direct test of the mailer, register ActionMailer::CaptureMethod and read ActionMailer::Base.deliveries in the same process:
ActionMailer::Base.delivery_method = ActionMailer::CaptureMethod
WelcomeMailer.welcome("sue@example.com").deliver_later
assert_equal 1, ActionMailer::Base.deliveries.lengthThe server is a separate process, so deliveries there is not visible to the harness. The file is the shared observation point for a live server; CaptureMethod is for a case that calls a mailer directly.
What is not included#
There are no templates, attachments, CC/BCC headers, or SMTP client. A message carries to, from, replyto, subject, and body. Durability and retry behavior depend on the job queue adapter and your provider; the mailer itself does not retry.
That is the whole path: build the message in a mailer, register a delivery class, and call deliver_now or deliver_later. Background Jobs covers what deliver_later hands the queue to.