Skip to main content
Build your first SMTP server with Fumi in just a few steps.

Basic server

1

Install Fumi

Add Fumi to your project using Bun:
bun add @puiusabin/fumi
2

Create your server

Create a new file server.ts with a basic SMTP server:
server.ts
import { Fumi } from "@puiusabin/fumi";

const app = new Fumi({ authOptional: true });

app.onMailFrom(async (ctx, next) => {
  if (ctx.address.address.endsWith("@blocked.example")) {
    ctx.reject("Domain blocked", 550);
  }
  await next();
});

await app.listen(2525);
console.log("SMTP server listening on port 2525");
This creates an SMTP server that:
  • Listens on port 2525
  • Allows unauthenticated connections
  • Blocks emails from @blocked.example domain
3

Run your server

Start the server with Bun:
bun server.ts

Testing your server

Test your SMTP server using telnet:
telnet localhost 2525
Then send SMTP commands:
EHLO localhost
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
Subject: Test Email

This is a test email.
.
QUIT
Press Ctrl+] then type quit to exit telnet if you get stuck.
You can also test with nodemailer:
test-client.ts
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: "localhost",
  port: 2525,
  secure: false,
  tls: {
    rejectUnauthorized: false,
  },
});

await transporter.sendMail({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Test Email",
  text: "This is a test email.",
});

console.log("Email sent successfully!");

Using plugins

Fumi comes with built-in plugins for common tasks. Here’s how to add logging and IP blocking:
import { Fumi } from "@puiusabin/fumi";
import { logger } from "@puiusabin/fumi/plugins/logger";
import { denylist } from "@puiusabin/fumi/plugins/denylist";

const app = new Fumi({ authOptional: true });

// Add logging
app.use(logger());

// Block specific IPs
app.use(denylist(["192.168.1.100", "10.0.0.50"]));

await app.listen(2525);
The logger plugin will output connection details:
[connect] 127.0.0.1
[mail from] [email protected]
[rcpt to] [email protected]
[close] 127.0.0.1

Adding custom middleware

Add your own middleware to any SMTP phase using the Koa-style (ctx, next) pattern:
import { Fumi } from "@puiusabin/fumi";

const app = new Fumi({ authOptional: true });

// Limit number of recipients
app.onRcptTo(async (ctx, next) => {
  const recipientCount = ctx.session.envelope.rcptTo.length;
  
  if (recipientCount >= 10) {
    ctx.reject("Too many recipients (max 10)", 452);
  }
  
  await next();
});

await app.listen(2525);
Middleware runs in the order it’s registered. Call await next() to continue the chain, or ctx.reject() to stop processing.

Common use cases

Block specific domains

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

Limit recipients per message

app.onRcptTo(async (ctx, next) => {
  if (ctx.session.envelope.rcptTo.length >= 5) {
    ctx.reject("Maximum 5 recipients allowed", 452);
  }
  await next();
});

Require authentication

const app = new Fumi({ authOptional: false });

app.onAuth(async (ctx, next) => {
  const { username, password } = ctx.credentials;
  
  // Check against your user database
  if (username === "user" && password === "pass") {
    ctx.accept({ username, role: "user" });
  } else {
    ctx.reject("Invalid credentials", 535);
  }
  
  await next();
});

Next steps

Core concepts

Learn about middleware, context, and sessions

Built-in plugins

Explore all available plugins

TLS setup

Configure TLS for secure connections

API reference

View complete API documentation

Build docs developers (and LLMs) love