memoryMailer records messages instead of sending them. testMailer is the contract suite every first-party adapter runs, and any adapter you write should pass it too.
In tests
import { memoryMailer } from "@byo-oss/mailer";
const mailer = memoryMailer();
await doSomethingThatSendsMail(mailer);
expect(mailer.sent[0]?.to).toBe("alice@example.com");mailer.sent holds copies of each message. mailer.clear() empties it.
memoryMailer rejects the reserved headers, as the adapters do: From, To, Cc, Bcc, Reply-To, Subject, Date, Message-ID, MIME-Version, Content-Type, Content-Transfer-Encoding, Return-Path and Sender, in any case. It throws a PermanentMailError and records nothing for that send. Before 0.2.0 it let them through; since 0.2.0 a test that passes one in headers fails: put the reply address in replyTo instead. See Delivery semantics.
| Option | Default | |
|---|---|---|
fail |
none | "retry" makes every send throw a plain error; "permanent" throws a PermanentMailError. Also settable later as mailer.fail. |
reportMessageId |
false |
Also return a messageId, as a provider that reports it would. |
Write your own adapter
Implement Mailer and run the contract suite against it, with vitest:
import { testMailer } from "@byo-oss/mailer/testing";
import { postmarkMailer } from "./mailer";
testMailer(() => postmarkMailer({ token: "test", fetch: okFetch }), {
failing: () => postmarkMailer({ token: "test", fetch: serverErrorFetch }),
permanentlyFailing: () =>
postmarkMailer({ token: "test", fetch: invalidAddressFetch }),
});failing is required, so the retry check always runs. @byo-oss/mailer/testing is ESM only.
The suite also sends malformed headers (a bad name, a value with CR, LF or another control character) and expects a PermanentMailError for each, so check headers before calling the provider. The rules are in Delivery semantics. memoryMailer checks them too.
Community adapters are separate packages named byo-mailer-<provider> (or the same under your own scope), and depend on this package’s types and nothing else of ours. Follow Delivery semantics: throw to retry, throw PermanentMailError to fail, never change the message, and never copy replyTo into headers.