Configuring Mail Relays
A relay is an SMTP endpoint plus the policy governing who may use it and what they may claim as a sender. Declaring one is an operator task: relays live in bondy.conf, not in realm state, because a realm administrator can never be allowed to grant their own realm access to a relay or read its credential.
This guide takes one from nothing to verified.
1. Declare the endpoint
mail.relay.transactional.host = smtp.example.com
mail.relay.transactional.port = 587
mail.relay.transactional.transport = starttlsstarttls is the default and requires the upgrade: if the relay will not offer STARTTLS the send fails rather than continuing in plaintext. Use tls for implicit TLS (usually port 465), and plain only for a relay on localhost.
Certificate verification is on by default and includes the hostname, so there is nothing to switch on. If the relay uses a private certificate authority, point at it:
mail.relay.transactional.tls.cacertfile = /etc/bondy/relay-ca.pemThat file has to be the authority that signed the relay's certificate, not the certificate itself — a self-signed leaf is refused however trusted it is.
2. Wire up the credential
Keep the password out of the configuration file. The env provider reads it from the environment:
mail.relay.transactional.auth = always
mail.relay.transactional.username = apikey
mail.relay.transactional.secret.provider = env
mail.relay.transactional.secret.env.var = BONDY_SMTP_PASSWORDaws_sm fetches it from AWS Secrets Manager instead, using secret.aws_sm.secret_id. A literal in bondy.conf (secret.provider = none with secret.value) works and is not forbidden — forbidding it only relocates the secret to BONDY_SMTP_PASSWORD=changeme — but Bondy will warn at boot naming the relay every time it starts.
A relay whose credential will not resolve is dropped
It does not start unauthenticated. An error is logged naming the relay and the resolver's reason, and the relay is absent from bondy.mail.relay.list — so a broken credential is visible rather than silent.
3. Decide who may send, and as whom
These two keys are the whole access-control story, and both default to closed.
mail.relay.transactional.from = no-reply@example.com
mail.relay.transactional.allowed_from = example.com
mail.relay.transactional.realms = com.acme.app, com.acme.adminrealms lists the realms permitted to use this relay. Leave it out and only the master realm can. * opens it to every realm.
from is the sender a caller gets when they do not ask for one, which is why a caller cannot spoof by default. allowed_from lists the domains a caller may ask for. Leave it out and callers cannot set the sender at all.
One line for a family of realms
If your tenant realms inherit from a prototype, name the prototype and every realm that inherits from it is admitted:
mail.relay.transactional.realms = com.acme.tenant_prototypeAdding a tenant realm then needs no change here. See Simplifying Realm Management Using Prototypes.
4. Size the pool and the queue
mail.relay.transactional.pool.size = 4
mail.relay.transactional.queue.max_size = 1000
mail.relay.transactional.rate_limit.rate = 20
mail.relay.transactional.rate_limit.burst = 50pool.size is how many messages this relay delivers concurrently. queue.max_size is the bound in front of it, across the whole pool — the workers divide it, they do not each get one — and when it is full, sends are refused immediately with bondy.error.mail_queue_full rather than blocking the caller. queue.max_bytes (64MB by default) bounds the same queue in memory, and whichever is reached first refuses; size it from the messages you actually send, because a thousand queued messages is a very different amount of memory with attachments than without.
The bondy_mail_queue_depth metric is that same count, so a dashboard reading near the bound is the signal to raise it or to add workers.
Set rate_limit.rate to whatever your provider's quota allows. It protects the relay, so refusal happens on Bondy's side of the queue rather than as a 4xx from the provider.
5. Name a default
mail.default_relay = transactionalWith exactly one relay configured this is unnecessary. With several and no default, a caller has to name one — Bondy does not guess, because guessing is how mail goes out through the wrong relay.
6. Verify it
Restart the node. From a master-realm session, list the relays and send yourself a test message:
bondy.mail.relay.list()
bondy.mail.test("com.leapsight.bondy", "you@example.com")bondy.mail.test is master-realm only. Its first argument is the realm whose relay sends the message; when you leave it out, Bondy uses the caller's realm. The procedure sends a fixed message, so nothing about its content can be the reason it fails. Pass relay as a keyword argument to test a specific one.
If it does not arrive, the error tells you which layer refused it:
| Error | What to change |
|---|---|
mail_not_configured | No relay is configured, or this one's credential would not resolve. Check the boot log. |
no_such_relay | The name is wrong, or several relays exist and mail.default_relay is unset. |
relay_not_permitted | mail.relay.$name.realms does not include the calling realm. |
sender_not_permitted | allowed_from does not include the domain that was asked for. |
relay_unavailable | Host, port, TLS or the certificate chain. Check tls.cacertfile. |
mail_rejected | The relay refused it. The code in the error is its reply code; the full text is in the log. |
7. Let a realm use it
Membership of mail.relay.$name.realms says the realm may use the relay. A session still needs the ordinary wamp.call permission on the procedure, which is granted like any other:
bondy.grant.create("com.acme.app", {
"permissions": ["wamp.call"],
"uri": "bondy.mail.",
"match": "prefix",
"roles": ["api_clients"]
})If you are using the broker bridge
Sending email on a published event needs one more key, and it belongs in this guide rather than the developer's because getting it wrong is an operational failure rather than a coding one:
broker_bridge.smtp.enabled = on
broker_bridge.config_file = ./etc/broker_bridge_config.jsonEnable it on every node
A broker bridge subscriber handles only locally-published events. A node where this is off silently drops the events published to it, so whether an email is sent depends on which node the publisher happened to connect to — which is the kind of fault that looks like an intermittent bug in the application for weeks.
The relay's realms list still applies: the subscription names a realm, and that realm has to be permitted to use the relay it names. See the Broker Bridge Configuration Reference.
Watching it in production
The Bondy — Mail dashboard is the per-relay view. Two things to know when reading it:
- Failures split by nature.
permanentmeans somebody has to change something;transientmeans the relay or the network is the problem. That split is the number that decides whether to page anyone. - A relay that fails repeatedly raises a
mail_relay_downalarm. Only transient failures count towards it, so a caller sending to a bad address cannot raise it. Delivery recovers on the first success; the alarm clears afterhealth.success_thresholdsuccesses, so a flapping relay does not flap the page.
See also
- Mail — the reasoning behind the model.
- Mail Configuration Reference — every key and its default.
- Sending Email — the developer's side.
- Sending Email with the SMTP Bridge — a worked example against a local mail server.