Envello
Tutorial

Sending email from Rails without ActionMailer's SMTP

Envello Team·2026-07-17·8 min read

Rails' ActionMailer defaults to SMTP delivery, which is fine for a lot of use cases but gives up the delivery visibility an HTTP API provides: structured bounce data, webhook events, and per-message status you can query. Worth being upfront: there's no dedicated Ruby SDK for Envello yet, unlike Node, Python, Go, and PHP. That's a real gap, not an oversight this post is going to pretend around, and a plain HTTP call covers the actual need with no extra dependency.

Replacing ActionMailer's delivery method

Keep ActionMailer for what it's good at: templating and organizing your mailer classes. Swap out the delivery step itself. Inside a mailer's deliver_now (or a custom delivery method), call Envello's API directly with Ruby's built-in Net::HTTP or a lightweight gem like Faraday, rather than routing through ActionMailer's SMTP adapter:

  • POST to https://api.envello.dev/emails with an Authorization: Bearer <ENVELLO_API_KEY> header
  • JSON body: { from:, to:, subject:, html: message.body.to_s }, reusing ActionMailer's own rendered HTML as the body
  • Check the response status and raise on a non-2xx rather than assuming success

Keeping ActionMailer's templating, dropping its SMTP delivery

This is a smaller change than it sounds like: your .html.erb mailer views, layouts, and mailer classes stay exactly as they are. Only the actual delivery mechanism, the part that currently hands off to an SMTP server, gets replaced with an HTTP POST. ActionMailer's delivery_method config supports a :test or custom adapter pattern if you want to keep the interception cleanly scoped to one place rather than editing every mailer.

Create a custom delivery method class that inherits from ActionMailer's interface and implements deliver!(mail). Inside deliver!, extract the recipient, subject, and rendered HTML from the Mail::Message object and make the HTTP call. Register the class in config/environments/production.rb: config.action_mailer.delivery_method = :envello.

Configuration

Store credentials in Rails credentials (rails credentials:edit) or environment variables. Never hardcode API keys.

  • Rails.application.credentials.envello[:api_key] for encrypted credentials
  • Or ENV['ENVELLO_API_KEY'] for environment-based config
  • Set the from address in an initializer or environment config: config.envello_from = 'YourApp <[email protected]>'

Error handling

The API returns standard HTTP status codes. Map them to Rails-appropriate error handling.

  • 2xx: accepted for delivery. Parse the JSON response for the message_id and log it.
  • 400: validation error. Don't retry. Log the response body (tells you what went wrong: bad address, unverified domain, suppressed recipient).
  • 429: rate limited. Raise a retryable error so Sidekiq/GoodJob can retry after a delay.
  • 5xx: server error. Raise a retryable error with a short backoff.
  • Network errors (Net::OpenTimeout, Net::ReadTimeout): retryable. The connection may have succeeded server-side; pass an idempotency key to prevent double-sends.

Background delivery with Sidekiq

For transactional emails triggered by web requests, deliver asynchronously. Rails' deliver_later works with Active Job, which delegates to your queue backend (Sidekiq, GoodJob, Solid Queue).

  • UserMailer.password_reset(user).deliver_later queues the job automatically
  • If using a custom delivery method, the deliver! method runs in the background worker's process
  • Sidekiq retries failed jobs with exponential backoff by default (25 retries over ~21 days)
  • For transactional email, cap retries lower: sidekiq_options retry: 3 in the job class, because a password reset email is useless 21 days later

Idempotency

A retried job that re-sends a password-reset email because the first HTTP call timed out (but actually succeeded server-side) is a worse outcome than a slightly delayed email. Passing an idempotency key, a hash of the mailer, recipient, and a relevant object ID, avoids double-sends on retry without needing to track delivery state yourself.

Generate the key: Digest::SHA256.hexdigest("#{mailer_class}:#{recipient}:#{record_id}:#{Date.current}"). Pass it as an X-Idempotency-Key header in the API request. The API deduplicates requests with the same key within a window.

Webhook handling

Set up a Rails controller to receive delivery event webhooks from Envello.

  • Create a WebhooksController with an envello action
  • Skip CSRF verification for the action: skip_before_action :verify_authenticity_token, only: :envello
  • Read the raw request body (request.raw_post) and the X-Envello-Signature header
  • Verify the HMAC-SHA256 signature: ActiveSupport::SecurityUtils.secure_compare(computed, header_value)
  • Parse the JSON payload and handle: delivered (log), bounced (suppress), complained (suppress)
  • Return head :ok immediately; process asynchronously with a background job if the handler does database work

Suppression tracking

Create an email_suppressions table to track bounced and complained addresses. Check this table before attempting a send to avoid wasting API calls and damaging sender reputation.

  • rails generate model EmailSuppression email:string:uniq reason:string suppressed_at:datetime
  • Before sending: return early if EmailSuppression.exists?(email: recipient)
  • In the webhook handler: EmailSuppression.find_or_create_by(email: event_email) { |s| s.reason = event_type; s.suppressed_at = Time.current }
  • Add an admin page (or Rails console scope) so support can look up why someone isn't getting mail

If a Ruby SDK matters enough to you

Node, Python, Go, and PHP SDKs already exist in Envello's repo (with varying publish status, none are fully production-published yet either). A Ruby client following the same shape is a reasonable thing to build if the plain HTTP approach above becomes a maintenance burden across many mailer classes, but for a typical Rails app's transactional email volume, a single shared HTTP helper method is usually enough.

Testing

For unit tests, use WebMock to stub the HTTP call and verify the request payload. ActionMailer's built-in test helpers (ActionMailer::Base.deliveries) work with the custom delivery method if you register a :test mode that captures messages instead of sending.

For integration tests, use Envello's test mode to validate the full send path without delivering to real inboxes. Verify the response parsing, error handling, and suppression logic end-to-end.

Free tool

Check your domain's SPF, DKIM, and DMARC records

Paste in a domain and see what's missing, plus the exact DNS records to fix it. Free, no account needed.

Check your domain →
Envello

EU-hosted transactional email, done right by default.