Overview
Message handling in the SMTP Server involves three main stages:
Envelope validation - Validating sender and recipients via onMailFrom and onRcptTo
Message reception - Receiving message content via onData
Response - Returning success or error to the client
Each stage provides hooks for implementing custom business logic like spam filtering, quota checks, and message storage.
The Message Flow
Validating Mail From
The onMailFrom handler validates the sender address before accepting the envelope:
Handler Signature
onMailFrom ( address , session , callback )
Address Object Structure
{
address : '[email protected] ' ,
args : {
SIZE : '12345' , // Message size from SIZE parameter
BODY : '8BITMIME' , // BODY parameter (7BIT or 8BITMIME)
SMTPUTF8 : true , // UTF-8 support requested
REQUIRETLS : true , // TLS required for delivery chain
RET : 'FULL' , // DSN return type (FULL or HDRS)
ENVID : 'ABC123' // DSN envelope ID
}
}
Implementation Examples
Accept All
Domain Validation
Size Limit Check
Deny List
const server = new SMTPServer ({
onMailFrom ( address , session , callback ) {
// Accept all senders
callback ();
}
});
Validating Recipients
The onRcptTo handler validates each recipient before accepting them:
Handler Signature
onRcptTo ( address , session , callback )
Address Object Structure
Implementation Examples
Mailbox Validation
Quota Check
Multiple Recipients
Relay Control
const server = new SMTPServer ({
async onRcptTo ( address , session , callback ) {
try {
const exists = await db . mailboxExists ( address . address );
if ( exists ) {
callback ();
} else {
const err = new Error ( 'Mailbox does not exist' );
err . responseCode = 550 ;
callback ( err );
}
} catch ( err ) {
callback ( err );
}
}
});
Handling Message Data
The onData handler receives the message content as a readable stream:
Handler Signature
onData ( stream , session , callback )
Stream Properties
Readable stream containing the message content. Special Properties:
stream.sizeExceeded - true if message exceeded size limit
Implementation Examples
Save to File
Parse and Process
Stream to Service
Basic Example (from source)
const server = new SMTPServer ({
onData ( stream , session , callback ) {
const messageId = generateMessageId ();
const fileName = `./messages/ ${ messageId } .eml` ;
const output = fs . createWriteStream ( fileName );
stream . pipe ( output );
stream . on ( 'end' , () => {
if ( stream . sizeExceeded ) {
const err = new Error ( 'Message exceeds maximum size' );
err . responseCode = 552 ;
return callback ( err );
}
callback ( null , 'Message queued as ' + messageId );
});
output . on ( 'error' , err => {
callback ( err );
});
}
});
Always check stream.sizeExceeded before accepting the message. The stream will continue even if size limit is exceeded to avoid hanging the connection.
Accessing Session Data
The session object provides context about the current transaction:
const server = new SMTPServer ({
onData ( stream , session , callback ) {
console . log ( 'Session ID:' , session . id );
console . log ( 'Client IP:' , session . remoteAddress );
console . log ( 'Client hostname:' , session . clientHostname );
console . log ( 'Authenticated user:' , session . user );
console . log ( 'Secure connection:' , session . secure );
console . log ( 'Transmission type:' , session . transmissionType );
// Envelope information
console . log ( 'From:' , session . envelope . mailFrom . address );
console . log ( 'To:' , session . envelope . rcptTo . map ( r => r . address ));
console . log ( 'Size:' , session . envelope . mailFrom . args . SIZE );
console . log ( 'Body type:' , session . envelope . bodyType );
console . log ( 'UTF-8:' , session . envelope . smtpUtf8 );
console . log ( 'Require TLS:' , session . envelope . requireTLS );
// DSN parameters (if not hidden)
if ( session . envelope . dsn ) {
console . log ( 'DSN RET:' , session . envelope . dsn . ret );
console . log ( 'DSN ENVID:' , session . envelope . dsn . envid );
}
// Custom data stored in earlier handlers
console . log ( 'Custom data:' , session . customData );
}
});
Envelope Structure
The session envelope contains all transaction data:
session . envelope = {
mailFrom: {
address: '[email protected] ' ,
args: {
SIZE: '12345' ,
BODY: '8BITMIME' ,
SMTPUTF8: true ,
REQUIRETLS: true ,
RET: 'FULL' ,
ENVID: 'ABC123'
}
},
rcptTo: [
{ address: '[email protected] ' , args: {} },
{ address: '[email protected] ' , args: {} }
],
bodyType: '8BITMIME' , // or '7BIT'
smtpUtf8: true , // UTF-8 support
requireTLS: true , // TLS required
dsn: { // If DSN not hidden
ret: 'FULL' , // or 'HDRS'
envid: 'ABC123'
}
}
Error Handling
Return appropriate SMTP response codes for different error conditions:
const server = new SMTPServer ({
onData ( stream , session , callback ) {
// Consume stream even on error
stream . on ( 'data' , () => {});
// Size exceeded (552)
if ( stream . sizeExceeded ) {
const err = new Error ( 'Message too large' );
err . responseCode = 552 ;
return callback ( err );
}
// Mailbox full (452 - temporary, or 552 - permanent)
const err = new Error ( 'Mailbox full' );
err . responseCode = 452 ; // Temporary failure
// err.responseCode = 552; // Permanent failure
// Spam detected (550)
const spamErr = new Error ( 'Message rejected as spam' );
spamErr . responseCode = 550 ;
// Virus detected (554)
const virusErr = new Error ( 'Message contains virus' );
virusErr . responseCode = 554 ;
// Server error (451)
const serverErr = new Error ( 'Server error, please try again' );
serverErr . responseCode = 451 ;
}
});
Common Response Codes
250 - Message accepted (success)
451 - Temporary failure, try again later
452 - Insufficient system storage (temporary)
550 - Mailbox unavailable / Rejected
552 - Storage allocation exceeded (permanent)
554 - Transaction failed
Complete Example
const { SMTPServer } = require ( 'smtp-server' );
const { simpleParser } = require ( 'mailparser' );
const fs = require ( 'fs' );
const server = new SMTPServer ({
size: 10 * 1024 * 1024 ,
onMailFrom ( address , session , callback ) {
// Validate sender
if ( address . address . includes ( 'spam' )) {
const err = new Error ( 'Sender rejected' );
err . responseCode = 550 ;
return callback ( err );
}
callback ();
},
async onRcptTo ( address , session , callback ) {
// Validate recipient
const exists = await checkMailboxExists ( address . address );
if ( ! exists ) {
const err = new Error ( 'Mailbox not found' );
err . responseCode = 550 ;
return callback ( err );
}
callback ();
},
async onData ( stream , session , callback ) {
try {
// Parse message
const parsed = await simpleParser ( stream );
// Check size
if ( stream . sizeExceeded ) {
throw Object . assign (
new Error ( 'Message too large' ),
{ responseCode: 552 }
);
}
// Store message
const messageId = await saveMessage ( parsed , session );
// Send success response
callback ( null , `Message queued as ${ messageId } ` );
} catch ( err ) {
callback ( err );
}
}
});
server . listen ( 2525 );