Action Mailer — Sending Emails in Rails Without the Headaches
Email is one of those features every app eventually needs and nobody particularly wants to build. Welcome emails, password resets, weekly digests, transactional notifications — the list grows quickly once your app is live. Action Mailer is Rails’s built-in answer, and it’s more capable than most developers give it credit for. You get HTML and plain-text emails, previews in the browser, background delivery, and a testing setup that doesn’t require an actual mail server.
The Basics — Generating a Mailer
Setup:
bin/rails generate mailer UserMailer
This creates app/mailers/user_mailer.rb, a base mailer, and corresponding view directories. The structure mirrors controllers: methods in the mailer class map to email templates in app/views/user_mailer/.
Example:
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome_email(user)
@user = user
@login_url = login_url
mail(
to: @user.email,
subject: "Welcome to FirstDev, #{@user.first_name}!"
)
end
end
Example:
<%# app/views/user_mailer/welcome_email.html.erb %>
<h1>Hey <%= @user.first_name %>,</h1>
<p>You're in. Here's what to do next:</p>
<ul>
<li><a href="<%= @login_url %>">Complete your profile</a></li>
<li>Read our getting started guide</li>
</ul>
<p>If you have questions, reply to this email. Real humans answer.</p>
Example:
<%# app/views/user_mailer/welcome_email.text.erb %>
Hey <%= @user.first_name %>,
You're in. Complete your profile: <%= @login_url %>
Questions? Reply to this email.
Action Mailer automatically sends both HTML and plain text versions. The plain text version matters — some clients and filters prefer it, and it’s what users see if HTML rendering fails.
Sending Emails
From a controller or service object:
Example:
# Deliver immediately (synchronous — blocks the request)
UserMailer.welcome_email(@user).deliver_now
# Deliver in the background via Active Job (preferred for production)
UserMailer.welcome_email(@user).deliver_later
# Schedule delivery for a specific time
UserMailer.welcome_email(@user).deliver_later(wait_until: 1.hour.from_now)
deliver_later is almost always the right choice in production. It enqueues the email as an Active Job, which means your request completes immediately and email delivery happens asynchronously. If delivery fails, Active Job’s retry mechanism handles it.
Configuring Delivery
Configure the mail delivery method per environment. In development, letter_opener or mailhog intercepts emails locally without sending them:
Setup:
# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
For production with an SMTP provider (Sendgrid, Postmark, AWS SES):
Setup:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.sendgrid.net",
port: 587,
domain: "firstdev.blog",
user_name: Rails.application.credentials.dig(:sendgrid, :username),
password: Rails.application.credentials.dig(:sendgrid, :api_key),
authentication: "plain",
enable_starttls_auto: true
}
Always store SMTP credentials in Rails credentials, never in environment config files or source control.
Email Previews
Action Mailer ships with previews — a browser interface for viewing emails without actually sending them. Define preview classes in test/mailers/previews/:
Example:
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome_email
UserMailer.welcome_email(User.first)
end
end
Navigate to http://localhost:3000/rails/mailers/user_mailer/welcome_email to see the rendered email exactly as it will appear. This is one of the most underused features in Rails — it eliminates the “send a test email and check your inbox” loop during development.
Layouts and Shared Styles
Action Mailer supports layouts just like views. The default layout lives at app/views/layouts/mailer.html.erb:
Example:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
body { font-family: -apple-system, sans-serif; color: #333; }
a { color: #5046e5; }
</style>
</head>
<body>
<%= yield %>
</body>
</html>
Inline styles are the only reliable approach for email clients — most strip external stylesheets. The premailer-rails gem automatically inlines your CSS during delivery, letting you write normal stylesheets that get converted to inline styles before sending.
Pro-Tip: Test your emails in real clients, not just browser previews. Gmail, Outlook, and Apple Mail render HTML email differently — Outlook in particular ignores many CSS properties. Use a service like Litmus or Email on Acid for cross-client testing before shipping new email templates. One hour of testing saves a week of user complaints.
Testing Mailers
Rails sets delivery_method to :test in the test environment, collecting all emails in ActionMailer::Base.deliveries without sending them.
Example:
# test/mailers/user_mailer_test.rb
class UserMailerTest < ActionMailer::TestCase
test "welcome email" do
user = users(:one)
email = UserMailer.welcome_email(user)
assert_emails 1 do
email.deliver_now
end
assert_equal [user.email], email.to
assert_equal "Welcome to FirstDev, #{user.first_name}!", email.subject
assert_match user.first_name, email.body.encoded
end
end
assert_emails verifies the delivery count. email.body.encoded contains the full rendered body for content assertions. Test the subject, recipient, and key content — don’t test the full HTML markup.
Conclusion
Action Mailer covers most email needs out of the box: templated emails, background delivery, browser previews, and a clean test interface. The gap between “sending email” and “sending email reliably” comes down to configuration choices — using deliver_later, storing credentials securely, and testing across clients. Get those right and email in Rails becomes a solved problem rather than a source of ongoing incidents.
FAQs
Q1: How do I attach files to emails in Action Mailer?
Use attachments in the mailer method: attachments['report.pdf'] = File.read('report.pdf'). For Active Storage files, use attachments.inline['image.png'] = blob.download. Attachments are added before calling mail().
Q2: Can I send emails from background jobs directly?
Yes — UserMailer.welcome_email(user).deliver_now works inside any job. But prefer calling deliver_later from the job rather than nesting jobs inside jobs, which creates unnecessary complexity.
Q3: How do I send the same email to multiple recipients?
Pass an array to the to: option: mail(to: users.map(&:email)). For large lists, send individual emails rather than bulk — it prevents one bad address from blocking the whole send, and allows per-recipient personalization.
Q4: What’s the difference between deliver_now and deliver_later?
deliver_now sends synchronously in the current request/thread — delivery failures raise immediately. deliver_later enqueues via Active Job — delivery is asynchronous, retried on failure, and doesn’t block the caller. Use deliver_later in production web requests.
Q5: How do I prevent emails from sending in development?
Set config.action_mailer.perform_deliveries = false in config/environments/development.rb. Or use letter_opener gem to intercept and display emails in the browser instead. The latter is more useful because you can still verify email content is correct.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀