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.

Overview

bun-smtp supports four SASL authentication methods: PLAIN, LOGIN, CRAM-MD5, and XOAUTH2. You can control which methods are available and enforce authentication requirements through configuration options.

Configuration Options

Control authentication behavior with these options:
const server = new SMTPServer({
  authMethods: ["PLAIN", "LOGIN", "CRAM-MD5", "XOAUTH2"],
  allowInsecureAuth: false, // require TLS before AUTH (default)
  authOptional: false,      // require AUTH (default)
});
By default, allowInsecureAuth is false, meaning clients must complete STARTTLS before authenticating. This prevents credentials from being transmitted over unencrypted connections.

Options Reference

OptionTypeDefaultDescription
authMethodsstring[]["PLAIN", "LOGIN"]Allowed SASL methods
authOptionalbooleanfalseAllow unauthenticated sessions
allowInsecureAuthbooleanfalseAllow AUTH over plain TCP (no TLS)
authRequiredMessagestringCustom message for 530 response

The onAuth Callback

The onAuth callback is invoked for every authentication attempt. Call callback(null, { user }) to accept or callback(new Error("reason")) to reject.
The onAuth callback is required if authOptional is false. Without it, all authentication attempts will fail with a 535 error.

PLAIN and LOGIN

Both PLAIN and LOGIN methods deliver credentials in the same format. The only difference is the wire protocol — both provide username and password fields.
1

Configure the server

Enable PLAIN and LOGIN in your server configuration:
const server = new SMTPServer({
  authMethods: ["PLAIN", "LOGIN"],
  onAuth(auth, session, callback) {
    // Handle authentication
  },
});
2

Implement the onAuth handler

Check the credentials and call the callback:
onAuth(auth, session, callback) {
  if (auth.method !== "PLAIN" && auth.method !== "LOGIN") {
    return callback(new Error("Unsupported method"));
  }
  
  if (auth.username === "user" && auth.password === "secret") {
    callback(null, { user: auth.username });
  } else {
    callback(new Error("Invalid credentials"));
  }
}
3

Handle the user object

The user object you pass is available on session.user for all subsequent callbacks:
onData(stream, session, callback) {
  console.log(session.user); // "user"
}

Auth Object Fields (PLAIN/LOGIN)

FieldTypeDescription
method"PLAIN" | "LOGIN"Which method the client used
usernamestringDecoded username
passwordstringDecoded password

CRAM-MD5

CRAM-MD5 provides challenge-response authentication without transmitting the password. The server sends a challenge, and the client responds with an HMAC-MD5 digest.
CRAM-MD5 requires you to have access to the plaintext password (or a reversibly encrypted version) to verify the response. If you only store password hashes, CRAM-MD5 won’t work.
const server = new SMTPServer({
  authMethods: ["CRAM-MD5"],
  onAuth(auth, session, callback) {
    if (auth.method !== "CRAM-MD5") {
      return callback(new Error("Unsupported method"));
    }
    
    // Look up the stored password for this user
    const storedPassword = lookupPassword(auth.username);
    
    if (auth.validatePassword(storedPassword)) {
      callback(null, { user: auth.username });
    } else {
      callback(new Error("Invalid credentials"));
    }
  },
});

Auth Object Fields (CRAM-MD5)

FieldTypeDescription
method"CRAM-MD5"Authentication method
usernamestringClient username
challengestringServer-generated challenge string
challengeResponsestringRaw response from the client
validatePassword(password)(string) => booleanReturns true if the password matches
The validatePassword() method computes HMAC-MD5 of the challenge using the provided password and compares it to the client’s response:
src/auth.ts
validatePassword(password: string): boolean {
  const hasher = new CryptoHasher("md5", password);
  return (
    hasher.update(challenge).digest("hex").toLowerCase() ===
    challengeResponse
  );
}

XOAUTH2

XOAUTH2 is used for OAuth2 bearer token authentication, commonly used with services like Gmail and Office 365.
const server = new SMTPServer({
  authMethods: ["XOAUTH2"],
  onAuth(auth, session, callback) {
    if (auth.method !== "XOAUTH2") {
      return callback(new Error("Unsupported method"));
    }
    
    verifyToken(auth.username, auth.accessToken)
      .then((user) => callback(null, { user }))
      .catch(() => {
        // Return data to trigger the XOAUTH2 re-challenge
        callback(new Error("Invalid token"), {
          data: { status: "401", schemes: "bearer", scope: "mail" },
        });
      });
  },
});

Auth Object Fields (XOAUTH2)

FieldTypeDescription
method"XOAUTH2"Authentication method
usernamestringUser email address
accessTokenstringOAuth2 bearer token
When authentication fails, you can pass a data object in the response to trigger an XOAUTH2 error challenge. This allows the client to understand why authentication failed and potentially retry with a refreshed token.

Storing the Authenticated User

Whatever you pass as user in the success response is stored on session.user for the rest of the connection. You can pass any value — a string, number, or object:
callback(null, { user: "user@example.com" });

// Later in onData:
onData(stream, session, callback) {
  console.log(session.user); // "user@example.com"
}

Custom Error Messages

You can customize error responses by setting responseCode on the error object:
onAuth(auth, session, callback) {
  const error = new Error("Account suspended");
  error.responseCode = 554;
  callback(error);
}

Security Best Practices

1

Require TLS

Always keep allowInsecureAuth: false in production to prevent credential theft.
2

Rate limit authentication attempts

Track failed attempts per IP address and implement exponential backoff:
const failedAttempts = new Map<string, number>();

onAuth(auth, session, callback) {
  const ip = session.remoteAddress;
  const attempts = failedAttempts.get(ip) || 0;
  
  if (attempts > 5) {
    return callback(new Error("Too many failed attempts"));
  }
  
  // Verify credentials...
  if (!valid) {
    failedAttempts.set(ip, attempts + 1);
    return callback(new Error("Invalid credentials"));
  }
  
  failedAttempts.delete(ip);
  callback(null, { user: auth.username });
}
3

Use secure password storage

Never store plaintext passwords. Use bcrypt, scrypt, or Argon2 for password hashing.

Multiple Authentication Methods

You can support multiple methods simultaneously. Use the auth.method field to determine which method the client used:
const server = new SMTPServer({
  authMethods: ["PLAIN", "LOGIN", "CRAM-MD5"],
  onAuth(auth, session, callback) {
    switch (auth.method) {
      case "PLAIN":
      case "LOGIN":
        // Handle password-based auth
        if (verifyPassword(auth.username, auth.password)) {
          callback(null, { user: auth.username });
        } else {
          callback(new Error("Invalid credentials"));
        }
        break;
        
      case "CRAM-MD5":
        // Handle challenge-response
        const storedPassword = getPassword(auth.username);
        if (auth.validatePassword(storedPassword)) {
          callback(null, { user: auth.username });
        } else {
          callback(new Error("Invalid credentials"));
        }
        break;
        
      default:
        callback(new Error("Unsupported method"));
    }
  },
});

Build docs developers (and LLMs) love