Skip to content

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

bash
bin/rails generate mailer User welcome   # no gem needed: ActionMailer SMTP and Net::HTTP are built in

2. Configure 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"),  # 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" }
end

3. Write 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   # synchronous

4. Handle errors

Ruby on Rails idioms for error handling, retries, and structured logging when sending through PostStack.

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.

ruby
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
end

Development, test and staging

Keep real delivery out of development and CI. `letter_opener` is a popular way to open emails in the browser locally.

ruby
# 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_ key

Send through the REST API with Net::HTTP

The REST API returns the email id (`em_…`) with a `202 Accepted`. Recipients are always an array.

ruby
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.