fumi
Core Concepts

Session

The Session object and how to share state across SMTP phases.

The session object is created when a client connects and passed through every middleware phase. It is the primary way to share state across the connection lifecycle.

server.ts
app.onConnect(async (ctx, next) => {
  console.log(ctx.session.remoteAddress); // e.g. "127.0.0.1"
  await next();
});

Fields

FieldTypeDescription
idstringUnique connection ID, generated per connection
remoteAddressstringClient IP address
clientHostnamestringHostname the client provided in EHLO/HELO
openingCommandstringThe greeting command used — "EHLO" or "HELO"
securebooleantrue after STARTTLS upgrade or on implicit TLS (SMTPS)
userunknownSet by ctx.accept(user) in onAuth. undefined if auth was skipped
envelopeEnvelopePopulated after MAIL FROM and RCPT TO complete
transmissionTypestring"SMTP", "ESMTP", or "ESMTPS" depending on connection type

Field availability

Not every field is available in every phase:

  • id, remoteAddress, clientHostname, openingCommand — available from onConnect onward
  • securefalse initially; becomes true after STARTTLS or on SMTPS connections
  • user — only available after ctx.accept(user) is called in onAuth
  • envelope — fully populated after onRcptTo completes; partial during onMailFrom
  • transmissionType — available from onMailFrom onward

Sharing state across phases

Since session is the same object throughout the connection, you can attach arbitrary data to session.user in onAuth and read it in later phases.

server.ts
type AuthUser = { id: number; role: string };

app.onAuth(async (ctx, next) => {
  const user = await db.findByCredentials(
    ctx.credentials.username,
    ctx.credentials.password,
  );
  if (!user) {
    ctx.reject("Bad credentials", 535);
  }
  ctx.accept(user as AuthUser);
  await next();
});

app.onMailFrom(async (ctx, next) => {
  const user = ctx.session.user as AuthUser | undefined;
  if (user?.role !== "sender") {
    ctx.reject("Not authorized to send mail", 550);
  }
  await next();
});

The envelope

session.envelope contains the parsed MAIL FROM and RCPT TO data once those commands complete.

server.ts
app.onData(async (ctx, next) => {
  const { mailFrom, rcptTo } = ctx.session.envelope;
  console.log("from:", mailFrom.address);
  console.log("to:", rcptTo.map((r) => r.address));
  await next();
});
FieldTypeDescription
envelope.mailFromAddressParsed sender address
envelope.rcptToAddress[]All accepted recipient addresses

Address fields

Each Address has two fields:

FieldTypeDescription
addressstringThe email address, e.g. "user@example.com"
argsRecord<string, unknown>Parameters parsed from the SMTP command, e.g. SIZE=1234 from MAIL FROM:<> SIZE=1234

On this page