1. Mailer setup for localhost/development #

Use gem 'letter_opener'. This way you can see how emails look when they are delivered.

# config/environments/development.rb
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  config.action_mailer.delivery_method = :letter_opener
  config.action_mailer.perform_deliveries = true

On top of that, gem 'letter_opener_web' provides a beautiful UI for emails in your tmp folder 🤩

gem letter opener web

If you still have an error missing host to link to, you can add this:

# app/config/development.rb
require "active_support/core_ext/integer/time"

+ Rails.application.routes.default_url_options[:host] = 'localhost:3000'

Rails.application.configure do

2. Mailer setup for test/staging #

You might want to test real email delivery in PR apps / staging. You should be sure that you do not deliver these test emails to real people! The easiest solution would be to use an AWS sandbox domain. This way the emails will be delivered only to users from your domain. In the below example we do not want to get out of the sandbox:


3. Mailer setup for production #

Check out my post: Sending emails in production with Amazon SES

4. Sending emails #

Generate a mailer:

rails g mailer post post_created

Always use deliver_later, not deliver_now:

# app/controllers/posts_controller.rb
PostMailer.with(user: current_user, post: @post).post_created.deliver_later

You can pass multiple params and attachments:

# app/mailers/post_mailer.rb
class PostMailer < ApplicationMailer
  def post_created
    @user = params[:user]
    @post = params[:post]
    @greeting = "Hi"
    # attach from assets
    attachments['logo.png'] = File.read('app/assets/images/logo.png')
    # attach from /public folder
    attachments.inline['logo.png'] = File.read("#{Rails.root}/public/images/logo.png")
    # attach an ical event
    attachments['calendar-event.ics'] = { mime_type: 'application/ics', content: icalendar.to_ical }
    # attach an ActiveStorage file
    file = @user.avatar
    attachments['avatar.png'] = { mime_type: file.blob.content_type, content: file.blob.download }

    # mail options
      from: "Yaroslav <hello@superails.com>",
      to: email_address_with_name(User.first.email, User.first.full_name), 
      cc: User.all.pluck(:email), 
      bcc: "secret@superails.com", 
      subject: "New post created"

Rendering attachments in the email html:

# app/views/post_mailer/post_created.html.erb
<%= asset_url('/images/games/game-card-backgrounds/football.png', host: 'https://superails.com') %>
<%= image_tag attachments['logo.png'].url, style: 'max-width: 16em;' %>
<%= image_tag attachments['avatar.png'].url, alt: 'My Photo', width: 100 %>

<%= @user.email %> create <%= @post.title %>

Styling emails with CSS is tricky: you can’t be sure that different email clients (outlook/gmail) render the CSS in the same way.

I usually style emails with plain CSS, not a CSS framework.

5. Preview emails #

Best way for previewing your email html without actually sending it to an inbox:

# test/mailers/previews/post_mailer_preview.rb
class PostMailerPreview < ActionMailer::Preview

  # Preview this email at http://localhost:3000/rails/mailers/post_mailer/post_created
  def post_created
    PostMailer.with(user: User.first, post: Post.first).post_created

6. Writing tests #

Minitest example:

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

class PostMailerTest < ActionMailer::TestCase
  test "post_created" do
    mail = PostMailer.post_created
    assert_equal "Post created", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Hi", mail.body.encoded

Rspec example:

# main/spec/mailers/game_spec.rb
require 'rails_helper'

RSpec.describe PostMailer, type: :mailer do
  let(:user) { create(:user) }
  let(:post) { create(:post) }

  describe 'reminder' do
    let(:mail) { PostMailer.with(user:, game:).post_created }

    it 'renders the headers' do
      expect(mail.subject).to match('vs')
      expect(mail.to).to eq([user.email])
      expect(mail.from).to eq(['hello@superails.com'])

    it 'renders the body' do
      expect(mail.body.encoded).to match('Post created')
      expect(mail.body.encoded).to match('logo')
      expect(mail.body.encoded).to match('avatar')

7. Updating default layout #

# app/mailers/application_mailer.rb
  default from: 'Yaro <hello@corsego.com>'
# app/views/layouts/mailer.html.erb
    <%= yield %>
+    Regards, Yaroslav Shmarov
+    <br>
+    hello@superails.com

That’s it! 🎉🥳🍾