Cloudflare Email Service
A small Ruby client for sending transactional email through the Cloudflare Email Service.
Two interchangeable transports: REST (default — zero dependencies, just
net/http) and SMTP (optional, via the mail
gem). Same send_email call either way; pick the transport in configuration.
Developed at Primevise.
Installation
bundle add cloudflare-email_service
Requires Ruby 3.2+. For the SMTP transport, also add the mail gem:
bundle add mail
Usage
Configure once with your Cloudflare API token (plus an account id for REST), then send:
require "cloudflare/email_service"
Cloudflare::EmailService.configure do |config|
config.account_id = ENV["CLOUDFLARE_ACCOUNT_ID"]
config.api_token = ENV["CLOUDFLARE_API_TOKEN"]
end
response = Cloudflare::EmailService.send_email(
from: "welcome@yourdomain.com",
to: "recipient@example.com",
subject: "Welcome!",
html: "<h1>Welcome</h1><p>Thanks for signing up.</p>",
text: "Welcome! Thanks for signing up.",
)
response.success? # => true
response.delivered # => ["recipient@example.com"]
Response also exposes #queued, #permanent_bounces, #errors, #status,
and the raw parsed #body.
[!TIP]
from,to,cc,bcc, andreply_toaccept a string ("a@x.com"or"Display Name <a@x.com>"), a hash ({ email:, name: }), or — forto/cc/bcc— an array of either. Add files withattachments: [{ content: Base64.strict_encode64(bytes), filename:, type: }]and arbitrary headers withheaders: { "In-Reply-To" => "<id>" }.[!CAUTION] The total message size (body + attachments) must not exceed 5 MiB.
Skip the global config and pass credentials per client when you send from more than one account:
Cloudflare::EmailService::Client.new(account_id: "...", api_token: "...") # REST
Cloudflare::EmailService::SMTPClient.new(api_token: "...") # SMTP
Credentials
Set credentials in configure (above) or through the environment:
export CLOUDFLARE_ACCOUNT_ID="your-account-id" # REST only
export CLOUDFLARE_API_TOKEN="your-api-token"
REST needs an Email Sending: Send token; SMTP needs Email Sending: Edit and
no account id.
Transports
Both transports accept the same send_email call and return the same
Response — they differ only in how the message reaches Cloudflare. Choose one
with config.transport.
REST (:rest, the default) posts JSON to the Cloudflare API over HTTPS
using only net/http from the standard library — no MIME assembly, no gems.
Needs an account_id and an Email Sending: Send token. The right default for
almost everything.
SMTP (:smtp) submits over smtp.mx.cloudflare.net:465 (implicit TLS), with
MIME built by the mail gem — loaded lazily,
only when this transport is used. Needs an Email Sending: Edit token and no
account id. Reach for it when your environment already speaks SMTP or only
allows SMTP egress.
Cloudflare::EmailService.configure do |config|
config.transport = :smtp # default: :rest
config.api_token = ENV["CLOUDFLARE_API_TOKEN"]
end
[!NOTE] Selecting
:smtpwithout theConfigurationErrortelling you to add it.
Rails
Adding the gem registers a :cloudflare delivery method automatically — just
point ActionMailer at it. No require, no initializer:
# config/environments/production.rb
config.action_mailer.delivery_method = :cloudflare
Credentials come from CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN in the
environment. To set them in code (or pick the SMTP transport), add an
initializer:
# config/initializers/cloudflare_email_service.rb
Cloudflare::EmailService.configure do |c|
c.account_id = Rails.application.credentials.dig(:cloudflare, :account_id)
c.api_token = Rails.application.credentials.dig(:cloudflare, :api_token)
# c.transport = :smtp # optional; defaults to :rest
end
Your mailers then send through Cloudflare unchanged. Prefer ActionMailer's
built-in :smtp delivery? Point it at Cloudflare with the settings helper:
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = Cloudflare::EmailService.smtp_settings(
api_token: Rails.application.credentials.dig(:cloudflare, :api_token),
)
Inbound email (Action Mailbox)
Receive mail too. Cloudflare delivers inbound mail to an app only through an
Email Worker,
so first enable
Email Routing
on your domain (it adds the MX/SPF/DKIM records). Then a Worker forwards each
message to a :cloudflare
Action Mailbox
ingress that ships with the gem. Three steps:
1. Require the ingress — opt-in, so it stays out of send-only apps:
# config/initializers/cloudflare_email_service.rb
require "cloudflare/email_service/action_mailbox"
2. Select it and set the shared signing secret — either
CLOUDFLARE_EMAIL_INGRESS_SECRET or the cloudflare.ingress_secret credential:
# config/environments/production.rb
config.action_mailbox.ingress = :cloudflare
The route POST /rails/action_mailbox/cloudflare/inbound_emails is registered
for you, and every request is verified by an HMAC-SHA256 signature with replay
protection.
3. Deploy an Email Worker that signs and forwards each message, and bind it
to an Email Routing rule (or catch-all). One ships with the gem — set your app
URL and give it the same CLOUDFLARE_EMAIL_INGRESS_SECRET, then deploy:
- In this repo:
templates/cloudflare_email_worker.js - From the installed gem:
Cloudflare::EmailService.worker_template_path
[!NOTE] The Worker sends
Content-Type: message/rfc822; the ingress rejects anything else with415 Unsupported Media Type.
Errors
Non-2xx responses (and unsuccessful payloads) raise a typed error — every one a
subclass of Cloudflare::EmailService::Error:
| Class | When |
|---|---|
ConfigurationError |
missing account_id / api_token |
ValidationError |
the message is missing required fields |
AuthenticationError |
HTTP 401 / 403 |
RequestError |
HTTP 400 / 422 and other 4xx |
RateLimitError |
HTTP 429 |
ServerError |
HTTP 5xx |
NetworkError |
connection, timeout, and TLS failures |
API errors also carry #status and #errors for context.
Development
bundle exec rake test # run the Minitest suite
bundle exec rubocop # lint
License
Released under the MIT License.