Each step of sending or receiving email is a separate package. The packages pass messages to each other as ReadableStream<Uint8Array>. mail, mime-stream, and smtp never hold a whole attachment in memory, but DKIM signing has to read the whole message first.
Sending:
mailbuilds a raw message with attachments and inline images.dkimsigns it.smtpsends it from any runtime with a TCP socket API.
Receiving:
mail-authchecks SPF, DKIM, DMARC, and ARC in one call.spf,dkim,dmarc, andarcdo each check on its own.mime-streamsplits a message into its parts.mime-headersdecodes encoded-words and parses address lists and header parameters.
quoted-printable, base64, and charset handle the body encodings.
Send a signed message from a Worker
import * as Dkim from '@portable/dkim';
import * as Mail from '@portable/mail';
import * as Smtp from '@portable/smtp';
import * as Streams from '@portable/streams';
import { connect } from 'cloudflare:sockets';
const message: Mail.Message = {
from: 'Ada <ada@example.com>',
to: 'grace@example.org',
subject: 'Report',
text: 'The report is attached.',
attachments: [
{ content: (await fetch(reportUrl)).body!, filename: 'report.pdf' },
],
};
const [forSigning, forSending] = Mail.encodeStream(message).tee();
const field = await Dkim.sign(forSigning, {
domain: 'example.com',
selector: 'mail',
privateKey: env.DKIM_KEY,
});
await Smtp.send(
{
...Mail.envelope(message),
message: Streams.concat([
[new TextEncoder().encode(field)],
forSending,
]),
},
{
connect,
host: 'smtp.example.com',
port: 587,
auth: { user: 'ada@example.com', pass: env.SMTP_PASSWORD },
},
);Dkim.sign must read the whole message before it returns, because the DKIM-Signature field goes before the message and contains a hash of the body. tee splits the stream in two. While Dkim.sign reads the first copy, tee queues the second in memory, and Smtp.send sends it after the signature field, so the whole message is held in memory once.