Bondy Connect
Skip to content

Mail ​

Send email from a WAMP session. See Mail for the model behind these procedures; this page is the call shapes.

Description ​

Every procedure takes the realm as argument 0. Bondy supplies it from the session when it is absent, and refuses a caller who supplies a realm that is not the session's own. The master realm is the single exception, which is how an operator acts on another realm's behalf.

So the two shapes below are equivalent for an ordinary session, and the second is what you write:

erlang
bondy.mail.send("com.acme.app", Request)   % explicit; the session's own realm
bondy.mail.send(Request)                   % realm taken from the session

Authorization

bondy.mail.* is gated by the standard wamp.call permission on the procedure URI, so it is grantable per realm and per group, with prototype inheritance, using bondy.grant. No mail-specific permission exists.

Two further checks happen inside Bondy and are not grantable through the API, because they belong to the operator rather than to the realm: whether the realm may use the named relay (mail.relay.$name.realms), and whether it may claim the sender it asked for (mail.relay.$name.allowed_from).

The request object ​

idstring

Idempotency key. Sending the same key twice reports the first message instead of sending another. The check is cluster-wide, and the window is mail.status.ttl.

relaystring

Relay name. Defaults to mail.default_relay. Bondy refuses a relay the calling realm may not use.

fromstring

Sender, as a bare address or as a display name and address: no-reply@acme.com or Acme Ltd no-reply@acme.com. Defaults to the configured sender of the relay. allowed_from is matched against the ADDRESS, so a display name grants nothing. A name may not contain a control character, a double quote or a backslash.

tolistREQUIRED

Recipient addresses. Syntax-validated before the request is queued.

cclist

Carbon-copy addresses.

bcclist

Blind carbon-copy addresses. Delivered to via the envelope and absent from every header.

reply_tostring

Reply-To, as a bare address or as a display name and address.

subjectstringREQUIRED

Subject line. Non-ASCII is encoded and decoded transparently.

textstring

Plain-text body. At least one of text or html is required.

htmlstring

HTML body. Supplying both produces a multipart/alternative message.

headersmap

Additional headers. A name or value containing CR, LF or NUL is REJECTED, not sanitised. Envelope and security headers (Bcc, Return-Path, Received, DKIM-Signature, Authentication-Results and similar) are refused.

attachmentslist

A list of objects with filename, content_type and data. data is base64 on the wire and is decoded before the size limit is applied.

prioritystring

normal (default) or low. A low priority message waits behind every normal one queued for the same relay, however long it has been waiting itself.

timeoutinteger

Deadline in milliseconds. Bounded by the timeout of the relay: a caller may ask for less, never more.

Unknown keys are rejected, not ignored. A misspelled subjcet produces bondy.error.invalid_request, whose details.keys names the unknown keys, rather than an email with no subject.

Procedures ​

bondy.mail.send(realm_uri, request) -> [result] ​

Send a message and wait until the relay has taken responsibility for it.

Call ​

Positional Args ​
0string

The realm uri. Supplied from the session when absent. When present, it is the session realm, or the session is in the master realm.

1mapREQUIRED

The request object, described above.

Keyword Args ​

None. The request is positional: the realm occupies argument 0, so a shape admitting both would make the first positional argument mean two different things.

Result ​

Positional Args ​
0mapREQUIRED

An object with id (the message id), status (sent), receipt (the acknowledgement from the relay) and attempts (how many tries it took). A request whose idempotency key was already used answers with duplicate: true and reports the first message instead.

A receipt is not a delivery guarantee

It means the relay accepted the message. What happens after that is between the relay and the recipient; Bondy does not process bounces.

Keyword Args ​

None.

bondy.mail.send_async(realm_uri, request) -> [result] ​

Queue a message and return without waiting.

Call ​

Positional Args ​

Identical to bondy.mail.send.

Keyword Args ​

None.

Result ​

Positional Args ​
0mapREQUIRED

An object with id (the message id, which bondy.mail.status.get takes) and status (queued).

What a successful return does not mean

It does not mean the relay accepted the message, or saw it. It does not mean the message will be delivered. And it does not survive a restart: the queue is in memory, so a node that stops loses whatever it was holding.

What it does mean is that the message passed validation and authority and was accepted into a bounded queue on this node.

Keyword Args ​

None.

bondy.mail.status.get(realm_uri, id) -> [result] ​

Report what is known about a message.

Call ​

Positional Args ​
0string

The realm uri. Supplied from the session when absent.

1stringREQUIRED

The message id returned by send or send_async.

Keyword Args ​

None.

Result ​

Positional Args ​
0mapREQUIRED

An object with status (queued, sent, failed, shed or unknown), and where known: relay, attempts, nature (permanent or transient) and error_class.

statusMeaning
queuedAccepted into a relay's queue on the owning node.
sentA relay took responsibility for the message. Not a delivery guarantee.
failedA relay was shown the message and it will not be delivered.
shedIt was dropped from the queue before any relay saw it: it outlived mail.relay.$name.queue.ttl, or the worker holding it stopped. error_class says which.
unknownSee below.

A shed message may be retried with the same idempotency key

failed and shed differ in one way that matters to a client: an idempotency key whose message failed is spent, and sending it again reports the failure rather than sending anything. A key whose message was shed may be used again, and doing so sends.

The rule behind both is the same — a key is consumed once a relay has been shown the message. Bondy cannot tell a relay that never saw a message from one that accepted it and dropped the connection, so a failure is not licence to send twice. A shed message was never offered to a relay at all.

unknown means four different things, on purpose

An id that never existed, a record that has aged out of mail.status.ttl, a message belonging to another realm, and a message whose owning node is unreachable all answer unknown.

Answering the same way for all four is deliberate: a caller cannot use this to discover whether another realm's message exists, and an unreachable node genuinely has an unknown answer because its queue was in memory.

Keyword Args ​

None.

bondy.mail.relay.list(realm_uri) -> [result] ​

List the relays the calling realm may use.

Call ​

Positional Args ​
0string

The realm uri. Supplied from the session when absent.

Keyword Args ​

None.

Result ​

Positional Args ​
0listREQUIRED

One object per relay, each with name, transport, status (up or down) and from. Never the host, the username or the credential.

The list is filtered, not annotated: a realm is not shown a relay it would be refused. A list of things you cannot have is an invitation to try them.

Keyword Args ​

None.

bondy.mail.test(realm_uri, address) -> [result] ​

Send a fixed test message, to prove a relay works.

Master realm only

This is an operator's check. It names a recipient directly and is refused for any other realm.

Call ​

Positional Args ​
0string

The realm uri to send as. Supplied from the session when absent.

1stringREQUIRED

The address to send the test message to.

Keyword Args ​
relaystring

Which relay to test. Defaults to mail.default_relay.

Result ​

Positional Args ​

As bondy.mail.send.

Keyword Args ​

None.

Errors ​

URINatureRaised when
bondy.error.mail_not_configuredPermanentNo relay is configured, or the named one cannot be used as configured.
bondy.error.no_such_relayPermanentThe named relay does not exist.
bondy.error.relay_not_permittedPermanentThe relay exists, but this realm may not use it.
bondy.error.sender_not_permittedPermanentThe requested from is outside the relay's allowed_from.
bondy.error.invalid_recipientPermanentAn address is not a valid address.
bondy.error.mail_rejectedPermanentThe relay refused the message with a 5xx.
bondy.error.mail_delivery_failedTransientDelivery failed after exhausting its retries or its deadline.
bondy.error.relay_unavailableTransientThe relay could not be reached.
bondy.error.mail_queue_fullTransientThe relay's queue is at its bound.

Other errors a mail call can raise, all of them in the Error Reference:

  • bondy.error.invalid_request — a malformed request, a refused header, or a key the request contract does not recognise. Permanent.
  • bondy.error.too_large_payload — the message, or its recipient count, exceeds what the relay accepts. Permanent.
  • bondy.error.rate_limit_exceeded — the relay's rate limit refused it. Transient.
  • bondy.error.request_timeout — a synchronous send reached its deadline before the relay answered. Transient, and it says nothing about whether the message was delivered: use status.get with the id.
  • bondy.error.unavailable — the node that owns this idempotency key could not be reached. Transient, and no message was sent.

No relay hostname, credential or SMTP banner ever appears in an error. A relay's rejection text is written by someone other than Bondy, so only the three-digit reply code survives — that is the part a caller can act on.

See also ​