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
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
Client connects
The server sends a 220 greeting and advertises PLAIN and LOGIN in the EHLO response.
Client attempts TLS
Since allowInsecureAuth: false, the client must use STARTTLS before AUTH is allowed.
Client authenticates
The client sends AUTH PLAIN or AUTH LOGIN with credentials.
Server validates
The onAuth callback checks credentials and accepts or rejects.
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
PLAIN / LOGIN
CRAM-MD5
XOAUTH2
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" ));
}
}
}
authMethods : [ "CRAM-MD5" ],
onAuth ( auth , session , callback ) {
if ( auth . method === "CRAM-MD5" ) {
const storedPassword = getPassword ( auth . username );
if ( auth . validatePassword ( storedPassword )) {
callback ( null , { user: auth . username });
} else {
callback ( new Error ( "Invalid credentials" ));
}
}
}
authMethods : [ "XOAUTH2" ],
onAuth ( auth , session , callback ) {
if ( auth . method === "XOAUTH2" ) {
verifyOAuthToken ( auth . username , auth . accessToken )
. then ( user => callback ( null , { user }))
. catch (() => {
callback ( new Error ( "Invalid token" ), {
data: { status: "401" , schemes: "bearer" }
});
});
}
}
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