Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/puiusabin/bun-smtp/llms.txt

Use this file to discover all available pages before exploring further.

This example demonstrates how to build an SMTP server that requires authentication using PLAIN or LOGIN methods.

Complete example

auth-server.ts
import { SMTPServer } from "bun-smtp";
import type { AuthObject, SMTPSession, AuthResponse } from "bun-smtp";

// In production, use a proper database
const users = new Map([
  ["alice", "secret123"],
  ["bob", "password456"],
]);

const server = new SMTPServer({
  authMethods: ["PLAIN", "LOGIN"],
  authOptional: false, // Require authentication
  allowInsecureAuth: false, // Require TLS before AUTH
  
  onAuth(auth: AuthObject, session: SMTPSession, callback) {
    console.log(`Auth attempt: ${auth.method} from ${session.remoteAddress}`);
    
    if (auth.method === "PLAIN" || auth.method === "LOGIN") {
      const storedPassword = users.get(auth.username);
      
      if (storedPassword && auth.password === storedPassword) {
        console.log(`✓ User ${auth.username} authenticated`);
        callback(null, { 
          user: { username: auth.username, ip: session.remoteAddress } 
        });
      } else {
        console.log(`✗ Invalid credentials for ${auth.username}`);
        const err = new Error("Invalid username or password") as any;
        err.responseCode = 535;
        callback(err);
      }
    } else {
      const err = new Error("Unsupported authentication method") as any;
      err.responseCode = 504;
      callback(err);
    }
  },
  
  onData(stream, session, callback) {
    async function saveEmail() {
      const chunks: Uint8Array[] = [];
      for await (const chunk of stream) {
        chunks.push(chunk);
      }
      
      const filename = `${session.user.username}-${Date.now()}.eml`;
      await Bun.write(filename, Buffer.concat(chunks));
      
      console.log(`Saved email from ${session.user.username} to ${filename}`);
      callback(null);
    }
    
    saveEmail().catch(callback);
  },
});

await server.listen(587);
console.log("Authenticated SMTP server listening on port 587");

Authentication flow

1

Client connects

The server sends a 220 greeting and advertises PLAIN and LOGIN in the EHLO response.
2

Client attempts TLS

Since allowInsecureAuth: false, the client must use STARTTLS before AUTH is allowed.
3

Client authenticates

The client sends AUTH PLAIN or AUTH LOGIN with credentials.
4

Server validates

The onAuth callback checks credentials and accepts or rejects.
5

Session continues

If authentication succeeds, session.user is set and the client can send mail.

Using the authenticated user

The user object you return in onAuth is available throughout the session:
onAuth(auth, session, callback) {
  callback(null, { 
    user: { 
      id: 42, 
      username: auth.username,
      email: `${auth.username}@example.com`,
      roles: ["sender"]
    } 
  });
}

// Later in other callbacks:
onMailFrom(address, session, callback) {
  console.log(`User ${session.user.username} sending from ${address.address}`);
  
  // Enforce sender restrictions
  if (address.address !== session.user.email) {
    return callback(new Error("You can only send from your own address"));
  }
  
  callback(null);
}

onData(stream, session, callback) {
  console.log(`Receiving mail from user ID ${session.user.id}`);
  // ... process stream
}

Supporting multiple auth methods

onAuth(auth, session, callback) {
  if (auth.method === "PLAIN" || auth.method === "LOGIN") {
    const valid = validateCredentials(auth.username, auth.password);
    if (valid) {
      callback(null, { user: auth.username });
    } else {
      callback(new Error("Invalid credentials"));
    }
  }
}
See the Authentication guide for complete details on each auth method.

Rate limiting

Limit authentication attempts per IP:
const attempts = new Map<string, number>();

const server = new SMTPServer({
  onConnect(session, callback) {
    const ip = session.remoteAddress;
    const count = attempts.get(ip) || 0;
    
    if (count > 10) {
      const err = new Error("Too many failed attempts") as any;
      err.responseCode = 421;
      return callback(err);
    }
    
    callback(null);
  },
  
  onAuth(auth, session, callback) {
    const ip = session.remoteAddress;
    
    if (validateCredentials(auth.username, auth.password)) {
      attempts.delete(ip); // Reset on success
      callback(null, { user: auth.username });
    } else {
      attempts.set(ip, (attempts.get(ip) || 0) + 1);
      callback(new Error("Invalid credentials"));
    }
  },
});

Testing authentication

Test with openssl to see the SMTP dialogue:
openssl s_client -starttls smtp -connect localhost:587
Then:
EHLO localhost
AUTH PLAIN
# Paste base64-encoded: \0username\0password
MAIL FROM:<alice@example.com>
RCPT TO:<bob@example.com>
DATA
Subject: Test

Hello!
.
QUIT
Generate the PLAIN auth string:
echo -ne '\0alice\0secret123' | base64
Use Bun’s built-in btoa() and atob() functions to encode/decode base64 in your code.

Require TLS for authentication

The server rejects AUTH attempts over plain TCP when allowInsecureAuth: false:
const server = new SMTPServer({
  allowInsecureAuth: false, // default
  onAuth(auth, session, callback) {
    // This callback is only called after STARTTLS completes
    console.log("Secure:", session.secure); // true
    // ... validate credentials
  },
});
Clients must use STARTTLS before sending AUTH commands.

Next steps

TLS configuration

Add STARTTLS and implicit TLS support

Authentication guide

Learn about CRAM-MD5 and XOAUTH2

Callbacks reference

Explore all lifecycle callbacks

Session object

Learn about the session object