Send email in Ruby on Rails with ActionMailer
Configure ActionMailer’s SMTP settings once, then send with deliver_later — the same setup on Rails 6, 7 and 8, plus plain Ruby without Rails.
To send email in Ruby on Rails you use ActionMailer, and the quickest way to route it through PostStack is to set ActionMailer’s SMTP delivery method to smtp.poststack.dev with your API key as the password. Your mailers, views, previews and deliver_later calls do not change, and the same config works on Rails 6, 7 and 8. If you need the email id back to match delivery and bounce webhooks, call the REST API with Net::HTTP — no gem required. For plain Ruby without Rails (Sinatra, Hanami, scripts), the mail gem sends over the same SMTP relay. All three are below, along with attachments, previews, per-environment config and error handling for deliver_later.
Quickstart#
Generate a mailer
bashbin/rails generate mailer User welcome # no gem needed: ActionMailer SMTP and Net::HTTP are built inConfigure ActionMailer SMTP settings
ruby# config/environments/production.rb (works on Rails 6, 7 and 8) Rails.application.configure do config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: "smtp.poststack.dev", port: 587, user_name: "poststack", # any non-empty value password: ENV.fetch("POSTSTACK_API_KEY"), # or Rails.application.credentials.dig(:poststack, :api_key) authentication: :plain, enable_starttls_auto: true, # STARTTLS on 587 (port 465: use tls: true instead) open_timeout: 5, read_timeout: 10 } config.action_mailer.raise_delivery_errors = true config.action_mailer.default_url_options = { host: "app.yourdomain.com", protocol: "https" } endWrite the mailer and send the email
ruby# app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base default from: "Acme <hello@yourdomain.com>" # must be on a verified domain layout "mailer" end # app/mailers/user_mailer.rb class UserMailer < ApplicationMailer def welcome @user = params[:user] mail(to: @user.email, subject: "Welcome to Acme") end end # app/views/user_mailer/welcome.html.erb -> the HTML part # app/views/user_mailer/welcome.text.erb -> the plain-text part # (with both present, Rails sends multipart/alternative automatically) # anywhere in your app — e.g. a controller after sign-up UserMailer.with(user: @user).welcome.deliver_later # via Active Job # UserMailer.with(user: @user).welcome.deliver_now # synchronousHandle errors
ruby# config/initializers/action_mailer_retries.rb # Retry transient SMTP failures from deliver_later — but not bad configuration. Rails.application.config.to_prepare do ActionMailer::MailDeliveryJob.class_eval do # 4xx replies, timeouts and dropped connections: try again with backoff retry_on Net::SMTPServerBusy, Net::ReadTimeout, Net::OpenTimeout, Errno::ECONNRESET, wait: 30.seconds, attempts: 5 # 535 = bad API key; 5xx = permanent rejection. Retrying will not help. discard_on Net::SMTPAuthenticationError, Net::SMTPFatalError do |job, error| Rails.logger.error("PostStack rejected #{job.arguments.first}: #{error.message}") end end end
Attachments, inline images and custom headers#
Add files with attachments["name"] = data before calling mail. PostStack accepts up to 10 attachments per message, 10 MB per file and 25 MB in total. X-PostStack-Tags, X-PostStack-Idempotency-Key and X-PostStack-Schedule headers are read by the relay.
class InvoiceMailer < ApplicationMailer
def receipt
@order = params[:order]
attachments["invoice-#{@order.number}.pdf"] = @order.invoice_pdf # binary string
attachments["terms.pdf"] = File.read(Rails.root.join("public/terms.pdf"))
attachments.inline["logo.png"] = File.read(Rails.root.join("app/assets/images/logo.png"))
# in the view: <%= image_tag attachments["logo.png"].url %>
headers["X-PostStack-Tags"] = "receipt,transactional"
headers["X-PostStack-Idempotency-Key"] = "receipt-#{@order.id}"
mail(
to: @order.email,
cc: "accounts@example.com",
reply_to: "billing@yourdomain.com",
subject: "Receipt for order ##{@order.number}"
)
end
endKeep the API key in Rails credentials#
Instead of an environment variable you can store the key in encrypted credentials (bin/rails credentials:edit --environment production) and read it in the SMTP settings.
# config/credentials/production.yml.enc (decrypted view)
poststack:
api_key: sk_live_...
# config/environments/production.rb
config.action_mailer.smtp_settings = {
address: "smtp.poststack.dev",
port: 587,
user_name: "poststack",
password: Rails.application.credentials.dig(:poststack, :api_key),
authentication: :plain,
enable_starttls_auto: true
}Preview emails in the browser#
Mailer previews render your HTML and text parts at /rails/mailers in development without sending anything.
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome
UserMailer.with(user: User.first).welcome
end
end
# open http://localhost:3000/rails/mailers/user_mailer/welcomeDevelopment, test and staging#
Keep real delivery out of development and CI. letter_opener is a popular way to open emails in the browser locally.
# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener # gem "letter_opener"
config.action_mailer.perform_deliveries = true
# config/environments/test.rb (Rails default)
config.action_mailer.delivery_method = :test
# test/mailers/user_mailer_test.rb
class UserMailerTest < ActionMailer::TestCase
test "welcome" do
user = users(:alice)
email = UserMailer.with(user: user).welcome
assert_emails(1) { email.deliver_now }
assert_equal ["hello@yourdomain.com"], email.from
assert_equal "Welcome to Acme", email.subject
end
end
# staging: same SMTP settings as production, with an sk_test_ keySend through the REST API with Net::HTTP#
The REST API returns the email id (em_…) with a 202 Accepted. Recipients are always an array.
require "net/http"
require "json"
require "securerandom"
class Poststack
URI_EMAILS = URI("https://api.poststack.dev/emails")
def self.send_email(payload, idempotency_key: SecureRandom.uuid, attempts: 3)
attempts.times do |attempt|
res = Net::HTTP.start(URI_EMAILS.host, URI_EMAILS.port, use_ssl: true,
open_timeout: 5, read_timeout: 10) do |http|
req = Net::HTTP::Post.new(URI_EMAILS)
req["Authorization"] = "Bearer #{ENV.fetch('POSTSTACK_API_KEY')}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = idempotency_key # same key on every retry
req.body = payload.to_json
http.request(req)
end
case res.code.to_i
when 200..299 then return JSON.parse(res.body) # => {"id"=>"em_..."}
when 429, 500..599
sleep(res["retry-after"]&.to_i || 2**attempt)
else
# 400 invalid payload, 401/403 key problem, 422 unverified domain / suppressed
raise "PostStack #{res.code}: #{res.body} (request #{res['x-request-id']})"
end
end
raise "PostStack: retries exhausted"
end
end
Poststack.send_email({ # braces: a positional hash, not keyword arguments
from: "Acme <hello@yourdomain.com>",
to: ["user@example.com"],
subject: "Hello from Ruby!",
html: "<h1>Welcome!</h1>",
text: "Welcome!"
})Send email from plain Ruby (no Rails)#
For Sinatra, Hanami, Roda or a script, the mail gem (the library ActionMailer is built on) talks to the same SMTP relay. It builds the multipart HTML/text message and attachments for you.
# gem install mail (or add gem "mail" to your Gemfile)
require "mail"
Mail.defaults do
delivery_method :smtp,
address: "smtp.poststack.dev",
port: 587,
user_name: "poststack",
password: ENV.fetch("POSTSTACK_API_KEY"),
authentication: :plain,
enable_starttls_auto: true
end
Mail.deliver do
from "Acme <hello@yourdomain.com>"
to "user@example.com"
subject "Hello from Ruby!"
text_part do
body "Welcome! Thanks for signing up."
end
html_part do
content_type "text/html; charset=UTF-8"
body "<h1>Welcome!</h1><p>Thanks for signing up.</p>"
end
add_file "./invoice.pdf" # optional attachment
endFramework integrations#
ActionMailer + SMTP
Set delivery_method = :smtp with the settings above. Every mailer — Devise confirmations and password resets included — sends through PostStack. Put the key in ENV or Rails credentials (Rails.application.credentials.dig(:poststack, :api_key)), never in the repo.
Active Job backends
deliver_later enqueues an ActionMailer::MailDeliveryJob. Use Sidekiq, GoodJob or Solid Queue in production and add retry_on/discard_on as shown so transient network errors retry while configuration errors do not.
REST API with Net::HTTP
For non-Rails Ruby apps, or when you need the em_… email id, POST JSON to https://api.poststack.dev/emails with Authorization: Bearer <key>. Send an Idempotency-Key header so a retried request never delivers twice.
Mailer previews and tests
Previews in test/mailers/previews (or spec/mailers/previews) render without sending. Tests use the :test delivery method and ActionMailer::Base.deliveries, so nothing reaches PostStack.
Common pitfalls#
Net::SMTPAuthenticationError: 535
The password must be a PostStack API key (sk_live_… / sk_test_…) with sending permission. Check that ENV["POSTSTACK_API_KEY"] is actually set in the process that runs your jobs, not only in the web process.
"Domain '…' not found for this team"
The From address must be on a domain verified in PostStack. Check default from: in ApplicationMailer, Devise’s config.mailer_sender, and any from: passed to mail.
Emails never leave development
deliver_later silently does nothing if perform_deliveries is false, and errors are swallowed unless raise_delivery_errors = true. In production also check that a job worker is actually running.
SSL errors or timeouts on connect
Use port 587 with enable_starttls_auto: true, or port 465 with tls: true — not 465 with STARTTLS. If connections time out, your host may block outbound SMTP; the REST API over HTTPS (443) avoids that.
FAQ#
How do I send email in Rails 6 (or 7, or 8)?
Generate a mailer (bin/rails generate mailer User welcome), set config.action_mailer.delivery_method = :smtp with PostStack’s SMTP settings in config/environments/production.rb, then call UserMailer.with(user: user).welcome.deliver_later. The configuration is the same across Rails 6, 7 and 8.
Do I need a gem?
Not in Rails. ActionMailer’s SMTP delivery method is built in, and the REST examples use Net::HTTP from the standard library. Outside Rails, the mail gem is the simplest way to send a multipart email over SMTP.
How do I send email from Ruby without Rails?
Use the mail gem: set Mail.defaults to the smtp.poststack.dev SMTP settings with your API key as the password, then call Mail.deliver with from, to, subject and text/HTML parts. Or POST JSON to the REST API with Net::HTTP.
deliver_now or deliver_later?
deliver_later in request paths, so the SMTP round-trip happens in a background job with retries. deliver_now in scripts, rake tasks and tests.
Does Devise work with PostStack?
Yes. Devise sends through ActionMailer, so confirmation, password-reset and unlock emails use your SMTP settings. Set config.mailer_sender in config/initializers/devise.rb to an address on your verified domain.
How do I handle rate limits on the REST API?
On a 429, wait for the number of seconds in the Retry-After header and retry with the same Idempotency-Key. The example above does this.
Ready to send from Ruby on Rails?
Create a free account and an API key — no card needed.