scaleway_tem-actionmailer

Send ActionMailer email through Scaleway Transactional Email (TEM), over its HTTP API rather than its SMTP relay.

Symfony and Laravel have had a TEM mailer for years. Ruby did not. This is it.

# Gemfile
gem "scaleway_tem-actionmailer"
# config/initializers/scaleway_tem.rb
ScalewayTem::ActionMailer.configure do |config|
  config.secret_key = Rails.application.credentials.dig(:scaleway, :tem, :secret_key)
  config.project_id = Rails.application.credentials.dig(:scaleway, :tem, :project_id)
end

# config/environments/production.rb
config.action_mailer.delivery_method = :scaleway_tem

That is the whole integration. Your mailers do not change.

Requirements: Ruby 3.2 or newer. Rails is optional, see Using it without Rails.

Why the API and not SMTP

Scaleway TEM offers both, and Rails can already speak SMTP with no gem at all. The HTTP API is worth a gem for one reason: it needs no outbound SMTP port. That matters in a container, and it matters on Scaleway Instances in particular, where outbound SMTP is blocked by default until you ask support to unblock it.

If port 587 is open where you deploy, you may not need this gem. Point ActionMailer at smtp.tem.scaleway.com with your Project ID as the username and an API secret key as the password, and you are done.

Installation

gem "scaleway_tem-actionmailer"

Configuration

# config/initializers/scaleway_tem.rb
ScalewayTem::ActionMailer.configure do |config|
  config.secret_key = Rails.application.credentials.dig(:scaleway, :tem, :secret_key)
  config.project_id = Rails.application.credentials.dig(:scaleway, :tem, :project_id)
  config.region     = "fr-par" # the default
end
# config/environments/production.rb
config.action_mailer.delivery_method = :scaleway_tem

The delivery method registers itself through a Railtie, so there is nothing else to wire up.

Setting Default Notes
secret_key none A Scaleway API secret key with TEM permissions
project_id none The project UUID the sending domain belongs to
region "fr-par"
timeout / open_timeout 15 Seconds
api_host https://api.scaleway.com Override for testing

Every setting can also be given per delivery through config.action_mailer.scaleway_tem_settings, which takes precedence.

What gets mapped

Mail Scaleway
from (exactly one) from
to, cc, bcc to, cc, bcc
subject subject
text and HTML parts text, html
attachments attachments, base64 encoded
Reply-To and any other header additional_headers

Scaleway's payload has no reply_to field, so Reply-To is forwarded as an additional header, which is what Scaleway's own documentation shows. Headers that already have a payload field of their own are not duplicated.

Two things raise rather than degrade quietly: a message with no From, and a message with more than one From, because Scaleway accepts exactly one sender and dropping the extra would send from an address you did not choose.

Attachments are capped by Scaleway at 2MB for the whole message. The gem does not enforce this; the API will tell you.

Using it without Rails

The gem depends on faraday and mail, not on actionmailer. A delivery method is only a duck type, two methods, and the Railtie is loaded only when Rails is already present. So any Mail::Message works:

require "scaleway_tem/action_mailer"
require "mail"

ScalewayTem::ActionMailer.configure do |config|
  config.secret_key = ENV.fetch("SCW_SECRET_KEY")
  config.project_id = ENV.fetch("SCW_DEFAULT_PROJECT_ID")
end

mail = Mail.new do
  from    "Sender <sender@example.com>"
  to      "reader@example.com"
  subject "Hello"
  body    "Plain body"
end

ScalewayTem::ActionMailer::DeliveryMethod.new.deliver!(mail)

Checking your credentials

ScalewayTem::ActionMailer::Client.new.list_domains
# => { success: true, error: nil, domains: [{ "name" => "example.com", ... }] }

This sends no email. It is the quickest way to tell a bad key from an unverified sending domain.

Errors

All inherit from ScalewayTem::ActionMailer::Error.

  • ConfigurationError: no secret key or no project id. Raised before any network call, because that is a deployment mistake and should not look like a delivery failure.
  • DeliveryError: Scaleway answered and refused. Carries #status and #api_message so you can branch (retry a 429, do not retry a 403).
  • ConnectionError: the request never completed. Nothing is known about whether the message was accepted, so a blind retry risks a duplicate.

deliver! raises, like the SMTP delivery method does. Set config.action_mailer.raise_delivery_errors = false if you would rather a failed email did not take down the request.

Before it will work

  1. Add and verify your sending domain in the TEM console.
  2. Publish the SPF, DKIM and DMARC records Scaleway gives you.
  3. Create an API key whose IAM policy includes TEM permissions.

Your from address must be on the verified domain, or every send is refused.

Development

bundle install
bundle exec rake test
bundle exec rubocop

The suite uses WebMock and asserts on the exact bytes on the wire. No test touches the network.

See CLAUDE.md for the design constraints worth knowing before changing anything, particularly around the payload contract.

Troubleshooting

What you see What it usually means
ConfigurationError The credentials never reached the gem. Check the initializer actually ran and that the values are not nil.
HTTP 401, denied authentication Wrong secret key, or you used the access key instead of the secret key.
HTTP 403 The key authenticates but its IAM policy lacks Transactional Email permissions.
HTTP 403 naming the sender or domain The from domain is not verified in the TEM console, or you are sending from a different domain than the one you verified.
HTTP 404 on the endpoint The region is wrong. Domains live in one region and the default here is fr-par.
Everything succeeds, no email arrives Accepted is not delivered. Check Transactional Email, Activity in the console: bounces show up there.
ConnectionError Outbound HTTPS to api.scaleway.com is blocked, or the timeout is too tight for your network.

list_domains is the fastest way to separate a bad key from an unverified domain, and it sends nothing.

Contributing

Issues and pull requests are welcome. Please keep the test suite green (bundle exec rake test) and RuboCop clean (bundle exec rubocop), and add a test with any behaviour change. The payload contract in MessageMapper is the part most worth covering.

License

MIT. Copyright (c) 2026 Ludovic Frank.