Skip to main content

Overview

Message handling in the SMTP Server involves three main stages:
  1. Envelope validation - Validating sender and recipients via onMailFrom and onRcptTo
  2. Message reception - Receiving message content via onData
  3. 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

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

{
  address: '[email protected]',
  args: {
    NOTIFY: 'SUCCESS,FAILURE', // DSN notification conditions
    ORCPT: 'rfc822;[email protected]' // Original recipient
  }
}

Implementation Examples

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

stream
ReadableStream
Readable stream containing the message content.Special Properties:
  • stream.sizeExceeded - true if message exceeded size limit

Implementation Examples

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);

Build docs developers (and LLMs) love