Send emails with Ruby on Rails
Learn how to send transactional emails using PostStack and Ruby on Rails.
Rails sends email with 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, or you are sending from plain Ruby (Sinatra, Hanami, scripts), call the REST API with `Net::HTTP` — no gem required. Both approaches are below.
1. Generate a mailer
bin/rails generate mailer User welcome # no gem needed: ActionMailer SMTP and Net::HTTP are built in2. Configure ActionMailer SMTP settings
# 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"), # sk_live_... / sk_test_...
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" }
end3. Write the mailer and send the email
# 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 # synchronous4. Handle errors
Ruby on Rails idioms for error handling, retries, and structured logging when sending through PostStack.
# 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
endAttachments, 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
endDevelopment, 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!"
})Framework 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.
Notes
- SMTP host `smtp.poststack.dev`, port 587 with STARTTLS or 465 with implicit TLS (`tls: true`); the password is your API key
- `deliver_later` needs an Active Job backend (Sidekiq, GoodJob, Solid Queue) in production — the default `:async` adapter loses jobs on restart
- An `sk_test_...` key accepts and logs mail without delivering it — use it in staging
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?
No. ActionMailer’s SMTP delivery method is built into Rails, and the REST examples use `Net::HTTP` from the standard library.
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.
Related guides
Ready to send emails with Ruby on Rails?
Create a free account and get your API key in under a minute.