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.

CI Gem Version Gem Downloads


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. = 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, and reply_to accept a string ("a@x.com" or "Display Name <a@x.com>"), a hash ({ email:, name: }), or — for to / cc / bcc — an array of either. Add files with attachments: [{ content: Base64.strict_encode64(bytes), filename:, type: }] and arbitrary headers with headers: { "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 :smtp without the mail gem installed raises a ConfigurationError telling 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. = 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:

[!NOTE] The Worker sends Content-Type: message/rfc822; the ingress rejects anything else with 415 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.