Skip to content
LogoLogo

Email

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:

  • mail builds a raw message with attachments and inline images.
  • dkim signs it.
  • smtp sends it from any runtime with a TCP socket API.

Receiving:

  • mail-auth checks SPF, DKIM, DMARC, and ARC in one call. spf, dkim, dmarc, and arc do each check on its own.
  • mime-stream splits a message into its parts.
  • mime-headers decodes 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.