Skip to content
LogoLogo

@portable/smtp

Functions

Smtp.connect

Opens an SMTP session: connects, reads the greeting, sends EHLO (or HELO), starts TLS and logs in.

async function connect(options: connect.Options): Promise<Client>;
OptionTypeDefaultDescription
connectConnectOpens the TCP connection, such as connect from cloudflare:sockets.
hoststring
portnumber465 when secure is 'on', otherwise 587
secure"on" | "starttls" | "off"'on' for port 465, otherwise 'starttls''on' starts TLS right away, 'starttls' requires the STARTTLS command, and 'off' never encrypts.
authAuth
allowInsecureAuthbooleanfalseSend credentials over an unencrypted connection. Without it, AUTH requires TLS.
namestring'[127.0.0.1]'The client's host name for EHLO.
lmtpbooleanfalseSpeak LMTP (RFC 2033): LHLO, and one reply per recipient after the message.
signalAbortSignalLimits connecting, the TLS handshake and authentication. Aborting rejects with signal.reason.
import * as Smtp from '@portable/smtp';
import { connect } from 'cloudflare:sockets';
 
await using client = await Smtp.connect({
  connect,
  host: 'smtp.example.com',
  auth: { user: 'me@example.com', pass: env.SMTP_PASSWORD },
  signal: AbortSignal.timeout(10_000),
});
const result = await client.send({
  from: 'me@example.com',
  to: ['you@example.com'],
  message,
});

Smtp.send

Connects, sends one message and closes the connection.

async function send(mail: Mail, options: send.Options): Promise<Result>;
OptionTypeDefaultDescription
connectConnectOpens the TCP connection, such as connect from cloudflare:sockets.
hoststring
portnumber465 when secure is 'on', otherwise 587
secure"on" | "starttls" | "off"'on' for port 465, otherwise 'starttls''on' starts TLS right away, 'starttls' requires the STARTTLS command, and 'off' never encrypts.
authAuth
allowInsecureAuthbooleanfalseSend credentials over an unencrypted connection. Without it, AUTH requires TLS.
namestring'[127.0.0.1]'The client's host name for EHLO.
lmtpbooleanfalseSpeak LMTP (RFC 2033): LHLO, and one reply per recipient after the message.
signalAbortSignalLimits connecting, the TLS handshake and authentication. Aborting rejects with signal.reason.
import * as Smtp from '@portable/smtp';
import { connect } from 'cloudflare:sockets';
 
const result = await Smtp.send(
  {
    from: 'me@example.com',
    to: 'you@example.com',
    message: 'Subject: Hi\r\n\r\nHello',
  },
  {
    connect,
    host: 'smtp.example.com',
    auth: { user: 'me@example.com', pass: env.SMTP_PASSWORD },
  },
);

Errors

Smtp.CommandError

Thrown when connecting, a command, or a message transfer fails.

class CommandError extends Errors.BaseError {
  constructor(options: CommandError.Options);
  readonly kind: string;
  readonly code: CommandError.Code;
  readonly command: string | undefined;
  readonly responseCode: number | undefined;
  readonly enhancedCode: string | undefined;
  readonly response: string | undefined;
  readonly rejected: readonly Rejection[] | undefined;
}
OptionTypeDefaultDescription
codeCode
messagestringWhat failed. The server's reply, if there was one, is appended.
commandstring
replyReply
rejectedreadonly Rejection[]
causeunknown
import * as Smtp from '@portable/smtp';
 
try {
  await client.send({ from, to, message });
} catch (error) {
  if (
    error instanceof Smtp.CommandError &&
    error.responseCode === 452
  ) {
    retryLater();
  } else {
    throw error;
  }
}

Types

Smtp.Rejection

A recipient the server refused, with its reply.

type Rejection = {
  recipient: string;
  responseCode: number;
  enhancedCode: string | undefined;
  response: string;
};

Smtp.Auth

Credentials for AUTH PLAIN or LOGIN, or an OAuth 2.0 access token for XOAUTH2.

type Auth = {
    user: string;
    pass: string;
    method?:  |  | undefined;
} | {
    user: string;
    accessToken: string;
};

Smtp.Client

An open SMTP session, from Smtp.connect. Calls run one at a time, in the order they are made. Where the runtime has Symbol.asyncDispose, await using calls close() at the end of the block.

type Client = AsyncDisposeMethod & {
  readonly secure: boolean;
  readonly extensions: readonly string[];
  send(mail: Mail, options?: Client.SendOptions): Promise<Result>;
  close(options?: Client.CloseOptions): Promise<void>;
};

Smtp.Dsn

RFC 3461 delivery status notification parameters. They are sent only when the server offers DSN.

type Dsn = {
    ret?:  |  | undefined;
    envid?: string | undefined;
    notify?:  | readonly ( |  | )[] | undefined;
};

Smtp.Mail

A message and its envelope.

type Mail = {
  from: string;
  to: string | readonly string[];
  message: ReadableStream<Uint8Array> | Uint8Array | string;
  size?: number | undefined;
  dsn?: Dsn | undefined;
  requireTls?: boolean | undefined;
};

Smtp.Result

The outcome of a send that reached at least one recipient.

type Result = {
  accepted: readonly string[];
  rejected: readonly Rejection[];
  response: string;
};

Smtp.Connect

Opens a TCP connection. Pass connect from cloudflare:sockets, or an adapter for Deno or Node (see the README).

type Connect = (
  address: {
    hostname: string;
    port: number;
  },
  options: SocketOptions,
) => Socket;

Smtp.Socket

A TCP connection with the shape of the WinterTC and Cloudflare Workers Sockets API. The socket that connect from cloudflare:sockets returns fits it as is.

type Socket = {
  readonly readable: ReadableStream<Uint8Array>;
  readonly writable: WritableStream<Uint8Array>;
  readonly opened: Promise<unknown>;
  readonly closed: Promise<unknown>;
  close(): Promise<unknown> | void;
  startTls(): Socket;
};

Smtp.SocketOptions

The options Connect receives. cloudflare:sockets reads them as they are.

type SocketOptions = {
    secureTransport:  |  | ;
    allowHalfOpen: boolean;
};