Skip to main content
Complete reference for all TypeScript types, interfaces, and classes exported by Fumi.

SMTPError

Custom error class for SMTP protocol errors. Extends the standard JavaScript Error class with an SMTP response code.
class SMTPError extends Error {
  responseCode: number;
  constructor(message: string, responseCode?: number);
}
message
string
required
Human-readable error message.
responseCode
number
default:"550"
SMTP response code (e.g., 550, 535, 552). Defaults to 550 if not specified.

Example

import { SMTPError } from "@puiusabin/fumi";

// Throw with default code (550)
throw new SMTPError("Mailbox unavailable");

// Throw with custom code
throw new SMTPError("Authentication failed", 535);
throw new SMTPError("Message too large", 552);

Core Interfaces

Address

Represents an email address with optional SMTP parameters.
interface Address {
  address: string;
  args: Record<string, unknown>;
}
address
string
required
The email address (e.g., “[email protected]”).
args
Record<string, unknown>
required
SMTP parameters passed with the address (e.g., SIZE, BODY).

Envelope

Contains the sender and recipient information for an email transaction.
interface Envelope {
  mailFrom: Address;
  rcptTo: Address[];
}
mailFrom
Address
required
The sender address from the MAIL FROM command.
rcptTo
Address[]
required
Array of recipient addresses from RCPT TO commands.

Session

Represents an active SMTP session with connection and transaction metadata.
interface Session {
  id: string;
  secure: boolean;
  remoteAddress: string;
  clientHostname: string;
  openingCommand: string;
  user: unknown;
  envelope: Envelope;
  transmissionType: string;
}
id
string
required
Unique identifier for this SMTP session.
secure
boolean
required
Whether the connection is using TLS/SSL.
remoteAddress
string
required
IP address of the connected client.
clientHostname
string
required
Hostname provided by the client in EHLO/HELO command.
openingCommand
string
required
The command used to initiate the session (“EHLO” or “HELO”).
user
unknown
required
User data set during authentication via ctx.accept(user).
envelope
Envelope
required
The email envelope containing sender and recipients.
transmissionType
string
required
Type of transmission (e.g., “ESMTP”, “SMTP”).

Credentials

Authentication credentials provided by the client.
interface Credentials {
  method: string;
  username: string;
  password: string;
  validatePassword: (password: string) => boolean;
}
method
string
required
Authentication method used (e.g., “PLAIN”, “LOGIN”).
username
string
required
Username provided by the client.
password
string
required
Password provided by the client (plain text after decoding).
validatePassword
(password: string) => boolean
required
Helper function to validate a password hash. Returns true if the password matches.

Context Types

Context objects are passed to middleware functions for each SMTP phase.

ConnectContext

Context for the connection phase.
interface ConnectContext {
  session: Session;
  reject(message?: string, code?: number): never;
}
session
Session
required
The current SMTP session.
reject
(message?: string, code?: number) => never
required
Rejects the connection and closes it. Default code is 550.

Example

app.onConnect(async (ctx, next) => {
  if (ctx.session.remoteAddress === "192.168.1.100") {
    ctx.reject("Access denied", 550);
  }
  await next();
});

AuthContext

Context for the authentication phase.
interface AuthContext {
  session: Session;
  credentials: Credentials;
  accept(user: unknown): void;
  reject(message?: string, code?: number): never;
}
session
Session
required
The current SMTP session.
credentials
Credentials
required
Authentication credentials from the client.
accept
(user: unknown) => void
required
Accepts the authentication and stores user data in session.user.
reject
(message?: string, code?: number) => never
required
Rejects the authentication. Default code is 535.

Example

app.onAuth(async (ctx, next) => {
  const user = await db.findUser(ctx.credentials.username);
  
  if (user && ctx.credentials.validatePassword(user.passwordHash)) {
    ctx.accept({ id: user.id, email: user.email });
  } else {
    ctx.reject("Invalid username or password", 535);
  }
  
  await next();
});

MailFromContext

Context for the MAIL FROM phase.
interface MailFromContext {
  session: Session;
  address: Address;
  reject(message?: string, code?: number): never;
}
session
Session
required
The current SMTP session.
address
Address
required
The sender address from the MAIL FROM command.
reject
(message?: string, code?: number) => never
required
Rejects the sender address. Default code is 550.

Example

app.onMailFrom(async (ctx, next) => {
  const blockedDomains = ["spam.com", "abuse.net"];
  const domain = ctx.address.address.split("@")[1];
  
  if (blockedDomains.includes(domain)) {
    ctx.reject("Sender domain blocked", 550);
  }
  
  await next();
});

RcptToContext

Context for the RCPT TO phase. Called once for each recipient.
interface RcptToContext {
  session: Session;
  address: Address;
  reject(message?: string, code?: number): never;
}
session
Session
required
The current SMTP session.
address
Address
required
The recipient address from the RCPT TO command.
reject
(message?: string, code?: number) => never
required
Rejects this specific recipient. Default code is 550.

Example

app.onRcptTo(async (ctx, next) => {
  const recipient = ctx.address.address;
  const exists = await db.mailboxExists(recipient);
  
  if (!exists) {
    ctx.reject("Mailbox not found", 550);
  }
  
  await next();
});

DataContext

Context for the DATA phase.
interface DataContext {
  session: Session;
  stream: ReadableStream<Uint8Array>;
  sizeExceeded: boolean;
  reject(message?: string, code?: number): never;
}
session
Session
required
The current SMTP session.
stream
ReadableStream<Uint8Array>
required
Readable stream of the message data. Must be consumed by piping to a writable stream.
sizeExceeded
boolean
required
Whether the message exceeded the size limit set in FumiOptions.size.
reject
(message?: string, code?: number) => never
required
Rejects the message data. Default code is 552.

Example

app.onData(async (ctx, next) => {
  await next();
  
  // Save to file
  const file = Bun.file(`./messages/${ctx.session.id}.eml`);
  await ctx.stream.pipeTo(file.writer());
  
  if (ctx.sizeExceeded) {
    ctx.reject("Message exceeds maximum size", 552);
  }
});

CloseContext

Context for the close phase.
interface CloseContext {
  session: Session;
}
session
Session
required
The SMTP session that is closing.

Example

app.onClose(async (ctx) => {
  console.log(`Connection closed: ${ctx.session.id}`);
  await cleanupSession(ctx.session.id);
});

Function Types

Middleware

Middleware function type for SMTP phase handlers.
type Middleware<T> = (
  ctx: T,
  next: () => Promise<void>
) => Promise<void>;
ctx
T
required
Context object for the current phase (ConnectContext, AuthContext, etc.).
next
() => Promise<void>
required
Function to call the next middleware in the chain. Must be awaited.
Returns: A Promise that resolves when the middleware completes.

Example

const myMiddleware: Middleware<ConnectContext> = async (ctx, next) => {
  console.log("Before next middleware");
  await next();
  console.log("After next middleware");
};

app.onConnect(myMiddleware);

Plugin

Plugin function type for registering reusable middleware bundles.
type Plugin = (app: Fumi) => void;
app
Fumi
required
The Fumi instance to register middleware on.

Example

import type { Plugin } from "@puiusabin/fumi";

function myPlugin(options: { prefix: string }): Plugin {
  return (app) => {
    app.onConnect(async (ctx, next) => {
      console.log(`${options.prefix}: Client connected`);
      await next();
    });
  };
}

// Usage
app.use(myPlugin({ prefix: "[SMTP]" }));

Configuration

FumiOptions

Configuration options for the Fumi SMTP server.
interface FumiOptions {
  // TLS
  secure?: boolean;
  key?: string | Buffer;
  cert?: string | Buffer;
  ca?: string | Buffer;
  requireTLS?: boolean;

  // Auth
  authMethods?: string[];
  authOptional?: boolean;

  // Limits
  size?: number;
  maxClients?: number;

  // Behavior
  banner?: string;
  disabledCommands?: string[];
  hideSTARTTLS?: boolean;
  hidePIPELINING?: boolean;
  hideENHANCEDSTATUSCODES?: boolean;
  hideDSN?: boolean;
  hideREQUIRETLS?: boolean;
  lmtp?: boolean;
  useXClient?: boolean;
  useXForward?: boolean;
  allowInsecureAuth?: boolean;
  closeTimeout?: number;
}

Example

import { Fumi } from "@puiusabin/fumi";

const app = new Fumi({
  // TLS configuration
  secure: false,
  requireTLS: true,
  key: await Bun.file("./server-key.pem").text(),
  cert: await Bun.file("./server-cert.pem").text(),
  
  // Authentication
  authMethods: ["PLAIN", "LOGIN"],
  authOptional: false,
  
  // Limits
  size: 25 * 1024 * 1024, // 25MB
  maxClients: 100,
  
  // Customization
  banner: "Welcome to MyMail SMTP Server",
  closeTimeout: 30000,
});

Build docs developers (and LLMs) love