Rails sends email through Action Mailer, which needs one smtp_settings hash to talk to any SMTP relay. This guide configures Postwing as that relay and covers mailer classes, HTML and text views, attachments, and moving delivery off the request with deliver_later.
| Setting | Value |
|---|---|
| SMTP host | smtp.postwing.app |
| Port | 587 |
| Encryption | STARTTLS (the connection is upgraded to TLS before login) |
| Username | The login of an SMTP token for your domain |
| Password | The password of that token — shown once, when the token is created |
Add the following to config/environments/production.rb (and to staging.rb, if you have one):
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true # do not fail silently
config.action_mailer.default_url_options = { host: "your-domain.com" }
config.action_mailer.smtp_settings = {
address: "smtp.postwing.app",
port: 587,
user_name: Rails.application.credentials.dig(:smtp, :user),
password: Rails.application.credentials.dig(:smtp, :password),
authentication: :plain,
enable_starttls_auto: true,
open_timeout: 10,
read_timeout: 10,
}For implicit TLS on port 465, swap the three transport keys:
# Implicit TLS on port 465 instead of STARTTLS
port: 465,
ssl: true,
enable_starttls_auto: false,Keep the credentials in Rails' encrypted credentials store:
# EDITOR=vim bin/rails credentials:edit
smtp:
user: token-login@your-domain.com
password: your-token-passwordfalse outside development, which means Action Mailer discards SMTP errors and a completely broken configuration looks identical to a working one. This is the single most common reason "Rails says it sent the email but nothing arrived". bin/rails generate mailer OrderMailer confirmed# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "Acme <noreply@your-domain.com>"
layout "mailer"
end
# app/mailers/order_mailer.rb
class OrderMailer < ApplicationMailer
def confirmed(order)
@order = order
mail(to: order.customer.email, subject: "Your order ##{order.id} is confirmed")
end
end Give every mailer both an HTML and a text view. Rails detects the pair by filename and builds a multipart/alternative message automatically — HTML with no text part is a spam signal:
<%# app/views/order_mailer/confirmed.html.erb %>
<h1>Order confirmed</h1>
<p>Thanks, <%= @order.customer.name %>. Your order ships tomorrow.</p>
<%# app/views/order_mailer/confirmed.text.erb %>
Order confirmed
Thanks, <%= @order.customer.name %>. Your order ships tomorrow.# Blocking — the request waits for the SMTP round trip.
OrderMailer.confirmed(@order).deliver_now
# Queued via Active Job — returns immediately, retried on failure.
OrderMailer.confirmed(@order).deliver_later
# Or after a delay
OrderMailer.confirmed(@order).deliver_later(wait: 10.minutes) Prefer deliver_later in anything triggered by a request. deliver_now holds the response open for the whole SMTP round trip, and turns a momentary relay hiccup into a 500 for the user.
class InvoiceMailer < ApplicationMailer
def monthly(invoice)
attachments["invoice.pdf"] = File.read(invoice.path)
# Inline, referenced from the view as attachments["logo.png"].url
attachments.inline["logo.png"] = File.read(Rails.root.join("app/assets/images/logo.png"))
mail(to: invoice.customer.email, subject: "Your invoice")
end
end Passing an array to to: puts every address in one To: header where recipients see each other. Loop instead:
# One message per recipient — nobody sees anybody else's address.
User.find_each do |user|
ReportMailer.weekly(user).deliver_later
end# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener # opens mail in the browser
# or simply
config.action_mailer.perform_deliveries = false In tests Rails collects everything in ActionMailer::Base.deliveries:
# test/mailers/order_mailer_test.rb
test "confirmation is delivered" do
assert_emails 1 do
OrderMailer.confirmed(orders(:one)).deliver_now
end
mail = ActionMailer::Base.deliveries.last
assert_equal ["customer@example.com"], mail.to
assert_match "confirmed", mail.subject
end| Error | Cause and fix |
|---|---|
| No error, no email | raise_delivery_errors is false. Set it to true to see the real failure. |
Net::SMTPAuthenticationError: 535 | Wrong user_name or password. |
Net::OpenTimeout | Outbound port blocked. Use 8587 (STARTTLS) or 8465 (implicit TLS). |
OpenSSL::SSL::SSLError: wrong version number | ssl: true on port 587, or STARTTLS on 465. Match the mode to the port. |
Links point at localhost:3000 | default_url_options[:host] is unset for that environment. |
Net::SMTPFatalError: 550 | The from domain is not verified, or the token does not cover it. |
raise_delivery_errors defaults to false outside development, so Action Mailer swallows every SMTP error. Set config.action_mailer.raise_delivery_errors = true and the real failure — authentication, timeout, rejected sender — surfaces immediately.
On port 587 use enable_starttls_auto: true, which upgrades the plain connection to TLS before login. On port 465 use ssl: true with enable_starttls_auto: false, because that port is encrypted from the first byte. Mixing them causes a handshake failure.
The user_name or password is wrong. user_name must be the full login of an SMTP token for your domain, and password the value shown once when the token was created. Store both in Rails credentials rather than in the environment file.
deliver_now opens the SMTP connection inside the current request, so the user waits for it and a transient failure becomes a 500. deliver_later enqueues an Active Job that a worker delivers, which keeps the request fast and retries automatically. Use deliver_later everywhere except in scripts and tests.
Mailer views have no request context, so *_url helpers fall back to whatever default_url_options says. Set config.action_mailer.default_url_options = { host: 'your-domain.com' } in each environment.
Many hosting providers block outbound ports 25, 465 and 587. Change the port in smtp_settings to 8587 for STARTTLS or 8465 for implicit TLS.