fumi
Core Concepts

Middleware

How fumi's per-phase middleware system works.

Middleware in fumi follows the koa-compose pattern: (ctx, next) => Promise<void>.

server.ts
app.onMailFrom(async (ctx, next) => {
  // runs before the next middleware
  console.log("from:", ctx.address.address);
  await next();
  // runs after the next middleware returns
});

Registering handlers

Chaining

Register multiple handlers by calling the same method multiple times. They execute in registration order.

server.ts
app.onConnect(async (ctx, next) => {
  console.log("first");
  await next();
});

app.onConnect(async (ctx, next) => {
  console.log("second");
  await next();
});

Calling next() more than once in the same middleware throws an error — the compose implementation guards against this.

Short-circuiting

Skip next() to stop the chain without an error. The phase will still succeed from the client's perspective.

server.ts
app.onMailFrom(async (ctx, next) => {
  if (cached(ctx.address.address)) {
    // skip further processing, accept immediately
    return;
  }
  await next();
});

Flow control

Rejection

Call ctx.reject() to send an SMTP error response and stop the chain. It throws internally — never return type.

server.ts
app.onRcptTo(async (ctx, next) => {
  const domain = ctx.address.address.split("@")[1] ?? "";
  if (!allowedDomains.has(domain)) {
    ctx.reject(`Recipient domain ${domain} not accepted`, 550);
  }
  await next();
});

Auth phase

onAuth is the only phase with ctx.accept(user). If the chain completes without calling accept() or reject(), the server returns 535.

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

Available contexts

Phasectx fields
onConnectsession, reject()
onAuthsession, credentials, accept(), reject()
onMailFromsession, address, reject()
onRcptTosession, address, reject()
onDatasession, stream, sizeExceeded, reject()
onClosesession

The session object carries id, remoteAddress, clientHostname, secure, user, and envelope across all phases.

On this page