Documentation menu

Ruby on Rails

Point Action Mailer at MailDev in development, preview mailers in the browser, and assert on delivered mail in system tests.

Add this to config/environments/development.rb:

config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

config.action_mailer.smtp_settings = {
  address: 'localhost',
  port: 1025,
  enable_starttls_auto: false
}

enable_starttls_auto: false is the line people miss. Without it, Action Mailer opportunistically upgrades the connection, and depending on your Ruby and OpenSSL versions that can fail against a self-signed or absent certificate.

raise_delivery_errors = true is worth having in development: Rails swallows delivery failures by default, so a misconfigured host produces silence rather than an exception.

Reading it from the environment

config.action_mailer.smtp_settings = {
  address: ENV.fetch('SMTP_HOST', 'localhost'),
  port: ENV.fetch('SMTP_PORT', 1025).to_i,
  enable_starttls_auto: false
}

Necessary as soon as Rails or MailDev runs in a container — see Docker.

Send a test message

bin/rails console
ActionMailer::Base.mail(
  from: 'app@example.com',
  to: 'user@test.com',
  subject: 'Hello from MailDev',
  body: 'It works!'
).deliver_now

Mailer previews versus MailDev

Rails has built-in mailer previews at /rails/mailers, and they are genuinely useful — but they render a template, they do not deliver a message. MailDev shows you the message that actually went over the wire: final headers, the MIME structure Rails built, both body parts, and the attachments as they were encoded.

Use previews while you iterate on markup; use MailDev to confirm what the application really sends, and to click through the links in it.

Multipart mail

Give a mailer both .html.erb and .text.erb templates and Action Mailer builds a multipart/alternative message. MailDev shows each part separately, which is how you notice that your text alternative is still the scaffold placeholder:

class UserMailer < ApplicationMailer
  default from: 'app@example.com'

  def welcome(user)
    @user = user
    mail(to: @user.email, subject: 'Welcome')
  end
end

Attachments

def invoice(user, pdf)
  attachments['invoice.pdf'] = pdf
  attachments.inline['logo.png'] = File.read(Rails.root.join('app/assets/images/logo.png'))
  mail(to: user.email, subject: 'Your invoice')
end

Inline attachments referenced with cid: render in MailDev's HTML preview, so an embedded logo shows up rather than appearing as a broken image.

Devise

Devise sends through Action Mailer, so confirmation, password-reset, and unlock emails land in MailDev with no additional configuration. Make sure config.action_mailer.default_url_options is set — otherwise the links in those emails have no host and are unusable.

Active Job

If deliveries are enqueued with deliver_later, mail only reaches MailDev once a job actually runs. In development, either use the inline adapter or keep a worker running:

config.active_job.queue_adapter = :inline

System tests

For unit tests, ActionMailer::Base.deliveries is the right tool and needs no external process.

MailDev is for the end-to-end case, where a real server process sends the mail. Read the REST API:

require 'net/http'
require 'json'

MAILDEV = URI('http://localhost:1080')

def clear_inbox
  Net::HTTP.start(MAILDEV.host, MAILDEV.port) do |http|
    http.request(Net::HTTP::Delete.new('/api/email/all'))
  end
end

def wait_for_email(subject, timeout: 10)
  deadline = Time.now + timeout
  while Time.now < deadline
    response = Net::HTTP.get(URI("#{MAILDEV}/api/email/summary?search=#{URI.encode_www_form_component(subject)}"))
    items = JSON.parse(response)['items']
    return JSON.parse(Net::HTTP.get(URI("#{MAILDEV}/api/email/#{items.first['id']}"))) if items.any?

    sleep 0.2
  end
  raise "No email matching #{subject.inspect} arrived"
end

More patterns in testing email in CI.

Letter Opener, and why you might still want MailDev

letter_opener writes each message to disk and opens it in a browser tab. It is frictionless for a solo Rails app.

MailDev is the better fit when mail comes from more than one service, when you want an inbox rather than a stream of tabs, when you need to assert on delivery from a test suite over HTTP, or when you want an AI agent to read the mail. They also compose fine — nothing stops you using previews, letter_opener, and MailDev on different environments.

Troubleshooting

Errno::ECONNREFUSED — MailDev is not running, or the address is wrong for your container setup.

EOFError or an OpenSSL error on deliveryenable_starttls_auto is still true. Set it to false.

Mail never arrives, no error. Either perform_deliveries is false, or the delivery method is still :test (the default in the test environment), or a deliver_later job is sitting in a queue with no worker.

Last updated 2026-08-25Edit this pageView as markdown