Skip to content

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#

Before you start

You need an API key (sk_live_…, or sk_test_… to try it without delivering anything) and a sending domain you have verified in PostStack. See Authentication and Domains.

  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"),  # 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" }
    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
    # 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

Good to know

  • 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

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

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

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

ruby
# 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/welcome

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!"
})

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.

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

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.

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.

Get started free

Next steps

Was this page helpful?