Delivery semantics

What a throw, a PermanentMailError and a result mean, and the rules for replyTo and headers.

Every adapter follows the same rules, so a package that calls send can retry without knowing the provider.

Errors

  • Throw means retry. Any error thrown from send tells the caller to try again later (a timeout, throttling, missing credentials, an unverified sender).
  • PermanentMailError means fail. It is thrown for a send that can never succeed, such as an invalid address or a message the provider rejected. The caller marks it failed without retrying. Check for it with isPermanentMailError(error), which also works when two copies of this package are installed.
  • At-least-once. A caller can repeat a send after a crash that came after the provider accepted it, so a recipient can rarely get a duplicate. A mailer doesn’t have to be idempotent.
  • The mailer never changes the message it’s given.

Each adapter’s own error mapping is on its page: Wraps, your own SES client, email-sdk.

replyTo and headers

replyTo is its own field. Adapters map it to the provider’s reply-to option and never copy it into headers.

headers is for extras such as In-Reply-To, References or List-Unsubscribe. wrapsMailer, sesMailer and emailSdkMailer reject the headers the provider owns (From, To, Cc, Bcc, Reply-To, Subject, Date, Message-ID, MIME-Version, Content-Type, Content-Transfer-Encoding, Return-Path, Sender) with a PermanentMailError.

memoryMailer rejects the same list, in any case, with a PermanentMailError, and records nothing for that send. Before 0.2.0 it only checked malformed headers; since 0.2.0 a test that passes one of these to memoryMailer fails. Use replyTo for the reply-to address.

Header names and values follow RFC 5322. A name is printable ASCII (33–126) without :. A value is printable ASCII, spaces and tabs, on one line: no CR or LF (so no folding), no other control character and no non-ASCII text (RFC 2047-encode it first). Every mailer here, memoryMailer included, throws a PermanentMailError for anything else before it calls the provider. The contract suite checks the name rules and the line-break and control-character rules for values. A tab is valid RFC 5322 and passes this check, but SES rejects it in a value (@wraps.dev/email before sending, SES v2 by its [ -~]* value pattern), so wrapsMailer and sesMailer fail such a send with a PermanentMailError too.

wrapsMailer and sesMailer send a display name in from that has non-ASCII characters as RFC 2047 encoded-words (Société <help@acme.com> goes out as =?UTF-8?B?U29jacOpdMOp?= <help@acme.com>), as the SES v1 SendEmail reference asks for Source, and quote an ASCII name that needs it (Acme, Inc.). An ASCII name that already holds an encoded-word is sent as given. The address itself is sent as given.

Result

send resolves to { id, messageId? }.

  • id is the provider’s id for the send when the provider reports one. When it doesn’t, emailSdkMailer makes up "<adapter>:<uuid>", and sesMailer and wrapsMailer make up "ses:<uuid>", rather than throwing, which would send the message again.
  • messageId is the RFC 5322 Message-ID actually sent, with angle brackets, only when the adapter knows it: wrapsMailer and sesMailer in us-east-1. See Wraps.
Edit this page on GitHub ↗@byo-oss/mailer 0.2.0