action-mailer

Installation
SKILL.md

Rails Action Mailer Expert

Create, configure, and deliver emails from Rails applications using Action Mailer.

Philosophy

Core Principles:

  1. Default to deliver_later — Queues emails via Active Job. deliver_now blocks the HTTP request, making users wait for SMTP round-trips.
  2. Create both HTML and text templates — Some email clients block HTML, and spam filters penalize HTML-only emails. Always provide a text fallback.
  3. Use parameterized mailers — Pass context via with(), not method arguments. This makes callbacks and shared setup work cleanly.
  4. Set default from: — Every mailer needs a default sender. Without it, emails fail silently or get rejected by mail servers.
  5. Write previews — Preview classes let you iterate on email design without sending real emails or writing test data by hand.
  6. Use _url helpers, not _path — Emails have no request context, so relative paths produce broken links. _url generates absolute URLs.

When To Use This Skill

  • Generating new mailer classes
  • Creating email templates (HTML + text)
  • Configuring SMTP/delivery settings
  • Adding attachments or inline images
  • Writing mailer previews and tests
  • Setting up interceptors or observers
  • Implementing I18n for email subjects
  • Debugging email delivery issues

Instructions

Step 1: Check Existing Mailer Patterns

Check the project first — match existing mailer patterns for consistency:

# Find existing mailers
ls app/mailers/

# Check ApplicationMailer defaults
cat app/mailers/application_mailer.rb

# Find existing templates
ls app/views/layouts/mailer.*
find app/views -name "*.html.erb" -path "*/mailer*" -o -name "*.text.erb" -path "*/mailer*"

# Check delivery config
grep -r "action_mailer" config/environments/

# Find existing previews
ls test/mailers/previews/

Consistency with the existing codebase matters more than theoretical best practice.

Step 2: Generate or Create the Mailer

Use the generator with action names:

bin/rails generate mailer User welcome_email password_reset

This creates:

  • app/mailers/user_mailer.rb — Mailer class
  • app/views/user_mailer/welcome_email.html.erb — HTML template
  • app/views/user_mailer/welcome_email.text.erb — Text template
  • app/views/user_mailer/password_reset.html.erb
  • app/views/user_mailer/password_reset.text.erb
  • test/mailers/user_mailer_test.rb — Test file
  • test/mailers/previews/user_mailer_preview.rb — Preview class

If ApplicationMailer doesn't exist, create it:

# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "notifications@example.com"
  layout "mailer"
end

Step 3: Write the Mailer Class

Use parameterized mailers (with/params pattern):

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  default from: "notifications@example.com"

  def welcome_email
    @user = params[:user]
    @login_url = login_url
    mail(
      to: email_address_with_name(@user.email, @user.name),
      subject: "Welcome to #{app_name}"
    )
  end

  def password_reset
    @user = params[:user]
    @token = params[:token]
    @reset_url = edit_password_reset_url(token: @token)
    mail(to: @user.email, subject: "Reset your password")
  end

  private

  def app_name
    Rails.application.class.module_parent_name
  end
end

Calling the mailer — prefer deliver_later:

# Preferred — async via Active Job (user doesn't wait for SMTP)
UserMailer.with(user: @user).welcome_email.deliver_later

# With params
UserMailer.with(user: @user, token: @token).password_reset.deliver_later

# Synchronous — blocks the request while talking to SMTP server
UserMailer.with(user: @user).welcome_email.deliver_now

Only use deliver_now for: rake tasks/cron jobs, console debugging, or critical emails needing immediate confirmation.

Step 4: Create BOTH Templates

Create both HTML and text versions. Action Mailer auto-generates multipart/alternative emails when both exist.

HTML template (app/views/user_mailer/welcome_email.html.erb):

<h1>Welcome, <%= @user.name %>!</h1>

<p>Thanks for signing up. Your account is ready.</p>

<p>
  <%= link_to "Log in to your account", @login_url %>
</p>

Text template (app/views/user_mailer/welcome_email.text.erb):

Welcome, <%= @user.name %>!

Thanks for signing up. Your account is ready.

Log in: <%= @login_url %>

Why both? Some email clients block HTML. Spam filters penalize HTML-only emails. Text fallback is a deliverability requirement.

Step 5: Configure URL Host

Emails need absolute URLs because there's no browser request context to resolve relative paths against:

# config/environments/development.rb
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

# config/environments/production.rb
config.action_mailer.default_url_options = { host: "www.example.com", protocol: "https" }

In templates, use _url helpers:

<%# CORRECT %>
<%= link_to "View order", order_url(@order) %>

<%# WRONG — will break in email %>
<%= link_to "View order", order_path(@order) %>

Step 6: Write Mailer Previews

Write a preview for each mailer action — this lets you iterate on design at /rails/mailers without sending real emails:

# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
  def welcome_email
    user = User.first || User.new(name: "Preview User", email: "preview@example.com")
    UserMailer.with(user: user).welcome_email
  end

  def password_reset
    user = User.first || User.new(name: "Preview User", email: "preview@example.com")
    UserMailer.with(user: user, token: "preview-token-123").password_reset
  end
end

Preview tips:

  • Use User.first with a fallback User.new(...) so previews work even with empty DB
  • Just return the mail object — don't call deliver_later in previews
  • Previews auto-reload on template changes

Step 7: Write Mailer Tests

# test/mailers/user_mailer_test.rb
require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  test "welcome_email" do
    user = users(:active_user)
    email = UserMailer.with(user: user).welcome_email

    assert_emails 1 do
      email.deliver_now
    end

    assert_equal ["notifications@example.com"], email.from
    assert_equal [user.email], email.to
    assert_equal "Welcome to MyApp", email.subject

    # Test HTML part
    assert_match user.name, email.html_part.body.to_s
    assert_match "Log in", email.html_part.body.to_s

    # Test text part
    assert_match user.name, email.text_part.body.to_s
  end
end

Key test assertions:

# Count emails sent
assert_emails 1 do
  UserMailer.with(user: user).welcome_email.deliver_now
end

# No emails sent
assert_no_emails do
  # action that shouldn't send email
end

# Enqueued for later delivery
assert_enqueued_emails 1 do
  UserMailer.with(user: user).welcome_email.deliver_later
end

# Check email content
assert_equal ["to@example.com"], email.to
assert_equal ["from@example.com"], email.from
assert_equal "Subject", email.subject
assert_match "expected text", email.body.encoded

Step 8: Configure Delivery Method

Development — use letter_opener or :test:

# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener # Opens in browser
# OR
config.action_mailer.delivery_method = :test # Stores in ActionMailer::Base.deliveries

Production — SMTP:

# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address:         "smtp.example.com",
  port:            587,
  domain:          "example.com",
  user_name:       Rails.application.credentials.dig(:smtp, :user_name),
  password:        Rails.application.credentials.dig(:smtp, :password),
  authentication:  "plain",
  enable_starttls: true,
  open_timeout:    5,
  read_timeout:    5
}

Test — always :test:

# config/environments/test.rb
config.action_mailer.delivery_method = :test

Quick Reference

mail() Method Options

Option Description
to: Recipient(s) — string or array
from: Sender (overrides default)
cc: Carbon copy recipients
bcc: Blind carbon copy recipients
reply_to: Reply-to address
subject: Email subject line
headers: Custom headers hash
template_path: Custom view directory
template_name: Custom template name
delivery_method_options: Per-email delivery overrides

Attachments

def invoice_email
  @invoice = params[:invoice]

  # Simple attachment
  attachments["invoice.pdf"] = File.read("/path/to/invoice.pdf")

  # Attachment with options
  attachments["report.csv"] = {
    mime_type: "text/csv",
    content: generate_csv_data
  }

  mail(to: @invoice.customer_email, subject: "Your Invoice")
end

Inline Attachments (Images in Email Body)

# In mailer
def newsletter
  attachments.inline["logo.png"] = File.read("app/assets/images/logo.png")
  mail(to: params[:user].email, subject: "Newsletter")
end
<%# In HTML template %>
<%= image_tag attachments["logo.png"].url, alt: "Company Logo" %>

Callbacks

class ApplicationMailer < ActionMailer::Base
  before_action :set_default_vars
  after_action  :log_delivery
  after_deliver :record_sent

  private

  def set_default_vars
    @company_name = "MyApp"
  end

  def log_delivery
    Rails.logger.info("Preparing email: #{action_name}")
  end

  def record_sent
    # Called after successful delivery
    EmailLog.create!(mailer: self.class.name, action: action_name)
  end
end

Interceptors and Observers

# config/initializers/mail_interceptors.rb
Rails.application.configure do
  if Rails.env.staging?
    config.action_mailer.interceptors = %w[SandboxEmailInterceptor]
  end
  config.action_mailer.observers = %w[EmailDeliveryObserver]
end

# app/interceptors/sandbox_email_interceptor.rb
class SandboxEmailInterceptor
  def self.delivering_email(message)
    message.to = ["sandbox@example.com"]
  end
end

# app/observers/email_delivery_observer.rb
class EmailDeliveryObserver
  def self.delivered_email(message)
    EmailDelivery.log(message)
  end
end

rescue_from in Mailers

class NotifierMailer < ApplicationMailer
  rescue_from ActiveJob::DeserializationError do |exception|
    # Handle stale records gracefully
  end

  rescue_from Net::SMTPAuthenticationError do |exception|
    # Handle SMTP auth failures
  end
end

I18n for Email Subjects

# config/locales/en.yml
en:
  user_mailer:
    welcome_email:
      subject: "Welcome to %{app_name}"
    password_reset:
      subject: "Reset your password"
# Mailer — omit subject: to auto-lookup from I18n
def welcome_email
  @user = params[:user]
  mail(to: @user.email) # Subject from en.user_mailer.welcome_email.subject
end

Parameterized Mailers for Shared Context

Use before_action + params to share context across multiple actions:

class InvitationsMailer < ApplicationMailer
  before_action { @inviter = params[:inviter]; @invitee = params[:invitee] }

  default to:   -> { @invitee.email },
          from: -> { email_address_with_name("invites@example.com", @inviter.name) }

  def account_invitation
    mail subject: "#{@inviter.name} invited you to #{params[:inviter].account.name}"
  end
end

# Call with: InvitationsMailer.with(inviter: user, invitee: other).account_invitation.deliver_later

Detailed References

For deeper patterns and examples, see the references/ directory:

  • references/templates.md — Mailer class patterns, HTML/text templates, attachments, I18n, layouts
  • references/delivery.md — SMTP configuration, per-environment setup, error handling
  • references/testing.md — Mailer test patterns, delivery assertions, integration tests
  • references/interceptors.md — Callbacks, interceptors, observers, delivery lifecycle
  • references/previews.md — Preview classes, fallback data, custom preview paths
  • references/configuration.md — Edge cases, gotchas, production recipes

Common Agent Mistakes

  1. Using deliver_now instead of deliver_later — Blocks the HTTP request while waiting for SMTP; users experience slow page loads
  2. Creating only HTML template — Spam filters penalize HTML-only emails, and some clients block HTML entirely
  3. Missing default from: address — Mail servers reject emails without a sender, often silently
  4. Using _path helpers in templates — Produces relative URLs that break in email clients (no request context to resolve against)
  5. Passing args instead of using with() — Parameterized mailers (Mailer.with(user: @user).action) work cleanly with callbacks and shared setup
  6. Forgetting to set default_url_options_url helpers raise errors or produce localhost links in production without host config
  7. Not writing previews — Without previews, you're sending real emails or writing tests just to see what an email looks like
  8. Not configuring delivery method per environment:test for test, :letter_opener for dev, :smtp for prod

Anti-Patterns to Avoid

  1. Fat mailers — Keep business logic in models/services. Mailers should just assemble and send; logic in mailers is hard to test and reuse
  2. Inline styles in templates — Use a mailer layout with shared styles; consider premailer-rails to auto-inline CSS for email client compatibility
  3. Hardcoded URLs — Route helpers with _url suffix stay correct when routes change; hardcoded strings rot
  4. No text fallback — HTML-only emails hurt deliverability scores and are inaccessible to screen readers
  5. Synchronous delivery in controllersdeliver_now in a request makes users wait for SMTP round-trips
  6. Untested mailers — Test content, recipients, subject, and delivery count
  7. Secrets in mailer views — Never expose tokens/passwords in emails; use short-lived, hashed links instead

File Structure

app/
  mailers/
    application_mailer.rb          # Base class — default from, layout
    user_mailer.rb                 # Mailer class
  views/
    layouts/
      mailer.html.erb              # HTML layout (wraps all mailer HTML views)
      mailer.text.erb              # Text layout
    user_mailer/
      welcome_email.html.erb       # HTML template
      welcome_email.text.erb       # Text template
config/
  environments/
    development.rb                 # delivery_method, default_url_options
    production.rb                  # SMTP settings
    test.rb                        # delivery_method: :test
test/
  mailers/
    user_mailer_test.rb            # Mailer tests
    previews/
      user_mailer_preview.rb       # Email previews
Related skills

More from thinkoodle/rails-skills

Installs
4
GitHub Stars
4
First Seen
9 days ago