Build your first SMTP server with Fumi in just a few steps.
Basic server
Install Fumi
Add Fumi to your project using Bun: Create your server
Create a new file server.ts with a basic SMTP server: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
Run your server
Start the server with Bun:
Testing your server
Test your SMTP server using telnet:
Then send SMTP commands:
Press Ctrl+] then type quit to exit telnet if you get stuck.
You can also test with nodemailer:
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:
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