/ Tags: RAILS / Categories: RAILS

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.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More