Send email from Rails
You probably already have mailers and .html.erb views, so the honest integration replaces the delivery method and nothing else. This page gives you the delivery class, the initializer that registers it, and a mailer that goes out over HTTP instead of SMTP.
Setup
- 01
Create a key
https://emails.sh/dashboard issues a key starting with esh_.
- 02
Store the key in credentials
bin/rails credentials:edit and add emailssh: api_key: esh_... Rails encrypts it into config/credentials.yml.enc, which is safe to commit. ENV works too if your host injects it.
- 03
Register the delivery method
config/initializers/emailssh.rb below adds :emailssh to ActionMailer. Existing mailers keep their views and their deliver_later calls.
- 04
Point the environment at it
config.action_mailer.delivery_method = :emailssh in config/environments/production.rb, and :test in test so your specs never send.
gem install emailsshRails.application.configure do
config.action_mailer.delivery_method = :emailssh
config.action_mailer.perform_deliveries = true
# Raise instead of swallowing, so a broken key shows up in your error tracker
# rather than as mail that quietly never arrives.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = { host: 'acme.com', protocol: 'https' }
endThe delivery method
lib/emailssh/delivery_method.rb (Rails 7.1 or 8)
require 'net/http'
require 'json'
require 'uri'
module Emailssh
# ActionMailer calls #deliver! with a Mail::Message. Everything ActionMailer
# already does (views, layouts, attachments, multipart) happens before this,
# so all that is left is turning the message into one HTTP request.
class DeliveryMethod
ENDPOINT = URI('https://emails.sh/v1/emails').freeze
attr_accessor :settings
def initialize(settings = {})
self.settings = settings
end
def deliver!(message)
payload = {
from: message[:from].to_s,
to: Array(message.to),
subject: message.subject,
html: html_part(message),
text: text_part(message),
cc: Array(message.cc),
bcc: Array(message.bcc),
reply_to: message[:reply_to]&.to_s
}.compact
request = Net::HTTP::Post.new(ENDPOINT)
request['Authorization'] = "Bearer #{settings.fetch(:api_key)}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate(payload)
response = Net::HTTP.start(ENDPOINT.hostname, ENDPOINT.port, use_ssl: true, read_timeout: 15) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
raise "emails.sh returned #{response.code}: #{response.body}"
end
JSON.parse(response.body)
end
private
def html_part(message)
return message.html_part.body.decoded if message.html_part
message.content_type&.start_with?('text/html') ? message.body.decoded : nil
end
def text_part(message)
return message.text_part.body.decoded if message.text_part
message.mime_type == 'text/plain' ? message.body.decoded : nil
end
end
endRegistering it
config/initializers/emailssh.rb
require Rails.root.join('lib/emailssh/delivery_method')
ActionMailer::Base.add_delivery_method(
:emailssh,
Emailssh::DeliveryMethod,
api_key: Rails.application.credentials.dig(:emailssh, :api_key) || ENV['EMAILSSH_API_KEY']
)A mailer that uses it
app/mailers/user_mailer.rb, unchanged from any other Rails app
class UserMailer < ApplicationMailer
default from: 'Acme <hello@acme.com>'
def welcome(user)
@user = user
@login_url = login_url
mail(to: @user.email, subject: 'Welcome to Acme')
end
end
# Call it from the controller. deliver_later hands it to Active Job so the HTTP
# request to emails.sh does not happen inside the user's request.
# UserMailer.welcome(user).deliver_laterWorth knowing
deliver_now blocks the request
deliver_now makes an HTTP call inline, so a slow response is a slow signup. Use deliver_later with Active Job on Solid Queue, Sidekiq, or GoodJob, and make sure a worker is actually running.
Set delivery_method to :test in test
Otherwise your suite sends real mail. config.action_mailer.delivery_method = :test in config/environments/test.rb, and assert on ActionMailer::Base.deliveries.
The from address must be a verified domain
default from: in the mailer sets the envelope, and if that domain is not verified on your workspace the API returns 422 invalid_from_domain. Use onboarding@emails.sh until verification finishes.
Credentials over ENV where you can
config/credentials.yml.enc is encrypted with the master key, so the API key is not in plaintext on disk or in your shell history. ENV is fine when the host injects it, which is why the initializer falls back.
Questions
How do I use emails.sh with ActionMailer?
Add a custom delivery method, as above. Your mailers, views, layouts, and previews all keep working, only the transport changes.
Do I have to replace ActionMailer?
No, and you should not. Rails already renders multipart mail from ERB. Replacing only deliver! keeps everything else you have.
Can I skip ActionMailer and call the API directly?
Yes, if you have no existing mailers. Net::HTTP or Faraday against https://emails.sh/v1/emails with a Bearer token is the whole thing.
How do I handle bounces in Rails?
Subscribe to the webhook at https://emails.sh/dashboard/webhooks and add a controller action for email.bounced and email.complained that marks the address unsendable.
The rest of the API
POST /v1/emails sends one and POST /v1/emails/batch sends up to 100 in a call. GET /v1/emails/:id returns the delivery events for a message, which is how you answer "did it arrive" without a support ticket. Webhooks push the same events to you as they happen.