'use strict';

const SMTPStream = require('./smtp-stream').SMTPStream;
const dns = require('dns');
const tls = require('tls');
const net = require('net');
const ipv6normalize = require('ipv6-normalize');
const sasl = require('./sasl');
const crypto = require('crypto');
const os = require('os');
const punycode = require('punycode.js');
const EventEmitter = require('events');
const base32 = require('base32.js');

const SOCKET_TIMEOUT = 60 * 1000;

// Enhanced Status Code mappings based on RFC 3463
const ENHANCED_STATUS_CODES = {
    // Success codes (2xx)
    200: '2.0.0', // System status, or system help reply
    211: '2.0.0', // System status, or system help reply
    214: '2.0.0', // Help message
    220: '2.0.0', // Service ready
    221: '2.0.0', // Service closing transmission channel
    235: '2.7.0', // Authentication successful
    250: '2.0.0', // Requested mail action okay, completed
    251: '2.1.5', // User not local; will forward
    252: '2.1.5', // Cannot VRFY user, but will accept message
    334: '3.7.0', // Server challenge for authentication
    354: '2.0.0', // Start mail input; end with <CRLF>.<CRLF>

    // Temporary failure codes (4xx)
    420: '4.4.2', // Timeout or connection lost (non-standard, used by some servers)
    421: '4.4.2', // Service not available, closing transmission channel
    450: '4.2.1', // Requested mail action not taken: mailbox unavailable
    451: '4.3.0', // Requested action aborted: local error in processing
    452: '4.2.2', // Requested action not taken: insufficient system storage
    454: '4.7.0', // Temporary authentication failure

    // Permanent failure codes (5xx)
    500: '5.5.2', // Syntax error, command unrecognized
    501: '5.5.4', // Syntax error in parameters or arguments
    502: '5.5.1', // Command not implemented
    503: '5.5.1', // Bad sequence of commands
    504: '5.5.4', // Command parameter not implemented
    521: '5.3.2', // Machine does not accept mail
    523: '5.3.4', // Message size exceeds server limit (non-standard, used by some servers)
    530: '5.7.0', // Authentication required
    535: '5.7.8', // Authentication credentials invalid
    538: '5.7.0', // Must issue a STARTTLS command first (non-standard)
    550: '5.1.1', // Requested action not taken: mailbox unavailable
    551: '5.1.6', // User not local; please try forwarding
    552: '5.2.2', // Requested mail action aborted: exceeded storage allocation
    553: '5.1.3', // Requested action not taken: mailbox name not allowed
    554: '5.6.0', // Transaction failed
    555: '5.5.4', // MAIL FROM/RCPT TO parameters not recognized or not implemented
    556: '5.1.10', // RCPT TO syntax error (non-standard)
    557: '5.7.1', // Delivery not authorized (non-standard, used by some servers)
    558: '5.2.3' // Message too large for recipient (non-standard, used by some servers)
};

// Skip enhanced status codes for initial greeting and HELO/EHLO responses
const SKIPPED_COMMANDS_FOR_ENHANCED_STATUS_CODES = new Set(['HELO', 'EHLO', 'LHLO']);

// Context-specific enhanced status code mappings
const CONTEXTUAL_STATUS_CODES = {
    // Mail transaction specific codes
    MAIL_FROM_OK: '2.1.0', // Originator address valid
    RCPT_TO_OK: '2.1.5', // Destination address valid
    DATA_OK: '2.6.0', // Message accepted for delivery

    // Authentication specific codes
    AUTH_SUCCESS: '2.7.0', // Authentication successful
    AUTH_REQUIRED: '5.7.0', // Authentication required
    AUTH_INVALID: '5.7.8', // Authentication credentials invalid

    // Policy specific codes
    POLICY_VIOLATION: '5.7.1', // Delivery not authorized
    SPAM_REJECTED: '5.7.1', // Message refused

    // Mailbox specific codes
    MAILBOX_FULL: '4.2.2', // Mailbox full
    MAILBOX_NOT_FOUND: '5.1.1', // Mailbox does not exist
    MAILBOX_SYNTAX_ERROR: '5.1.3', // Invalid mailbox syntax

    // System specific codes
    SYSTEM_ERROR: '4.3.0', // System error
    SYSTEM_FULL: '4.3.1', // System storage exceeded

    // Network specific codes
    NETWORK_ERROR: '4.4.0', // Network routing error
    CONNECTION_TIMEOUT: '4.4.2' // Connection timeout
};

/**
 * Creates a handler for new socket
 *
 * @constructor
 * @param {Object} server Server instance
 * @param {Object} socket Socket instance
 */
class SMTPConnection extends EventEmitter {
    constructor(server, socket, options) {
        super();

        options = options || {};
        // Random session ID, used for logging
        this.id = options.id || base32.encode(crypto.randomBytes(10)).toLowerCase();

        this.ignore = options.ignore;

        this._server = server;
        this._socket = socket;

        // session data (envelope, user etc.)
        this.session = this.session = {
            id: this.id
        };

        // how many messages have been processed
        this._transactionCounter = 0;

        // Do not allow input from client until initial greeting has been sent
        this._ready = false;

        // If true then the connection is currently being upgraded to TLS
        this._upgrading = false;

        // Set handler for incoming command and handler bypass detection by command name
        this._nextHandler = false;

        // Parser instance for the incoming stream
        this._parser = new SMTPStream();

        // Set handler for incoming commands
        this._parser.oncommand = (...args) => this._onCommand(...args);

        // if currently in data mode, this stream gets the content of incoming message
        this._dataStream = false;

        // If true, then the connection is using TLS
        this.session.secure = this.secure = !!this._server.options.secure;

        this.needsUpgrade = !!this._server.options.needsUpgrade;

        this.tlsOptions = this.secure && !this.needsUpgrade && this._socket.getCipher ? this._socket.getCipher() : false;

        // Store local and remote addresses for later usage
        this.localAddress = (options.localAddress || this._socket.localAddress || '').replace(/^::ffff:/, '');
        this.localPort = Number(options.localPort || this._socket.localPort) || 0;
        this.remoteAddress = (options.remoteAddress || this._socket.remoteAddress || '').replace(/^::ffff:/, '');
        this.remotePort = Number(options.remotePort || this._socket.remotePort) || 0;

        // normalize IPv6 addresses
        if (this.localAddress && net.isIPv6(this.localAddress)) {
            this.localAddress = ipv6normalize(this.localAddress);
        }
        if (this.remoteAddress && net.isIPv6(this.remoteAddress)) {
            this.remoteAddress = ipv6normalize(this.remoteAddress);
        }

        // Error counter - if too many commands in non-authenticated state are used, then disconnect
        this._unauthenticatedCommands = 0;

        // Max allowed unauthenticated commands
        this._maxAllowedUnauthenticatedCommands = this._server.options.maxAllowedUnauthenticatedCommands || 10;

        // Error counter - if too many invalid commands are used, then disconnect
        this._unrecognizedCommands = 0;

        // Server hostname for the greegins
        this.name = this._server.options.name || os.hostname();

        // Resolved hostname for remote IP address
        this.clientHostname = false;

        // The opening SMTP command (HELO, EHLO or LHLO)
        this.openingCommand = false;

        // The hostname client identifies itself with
        this.hostNameAppearsAs = false;

        // data passed from XCLIENT command
        this._xClient = new Map();

        // data passed from XFORWARD command
        this._xForward = new Map();

        // if true then can emit connection info
        this._canEmitConnection = true;

        // increment connection count
        this._closing = false;
        this._closed = false;
    }

    /**
     * Initiates the connection. Checks connection limits and reverse resolves client hostname. The client
     * is not allowed to send anything before init has finished otherwise 'You talk too soon' error is returned
     */
    init() {
        // Setup event handlers for the socket
        this._setListeners(() => {
            // Check that connection limit is not exceeded
            if (this._server.options.maxClients && this._server.connections.size > this._server.options.maxClients) {
                return this.send(421, this.name + ' Too many connected clients, try again in a moment', false);
            }

            // Keep a small delay for detecting early talkers
            let readyTimer = setTimeout(() => this.connectionReady(), 100);
            // Unref timer so connection init delay doesn't prevent process exit
            readyTimer.unref();
        });
    }

    connectionReady(next) {
        // Resolve hostname for the remote IP
        let reverseCb = (err, hostnames) => {
            if (err) {
                this._server.logger.error(
                    {
                        tnx: 'connection',
                        cid: this.id,
                        host: this.remoteAddress,
                        hostname: this.clientHostname,
                        err
                    },
                    'Reverse resolve for %s: %s',
                    this.remoteAddress,
                    err.message
                );
                // ignore resolve error
            }

            if (this._closing || this._closed) {
                return;
            }

            this.clientHostname = (hostnames && hostnames.shift()) || '[' + this.remoteAddress + ']';

            this._resetSession();

            let onSecureIfNeeded = next => {
                if (!this.session.secure) {
                    // no TLS
                    return next();
                }

                this.session.servername = this._socket.servername;
                this._server.onSecure(this._socket, this.session, err => {
                    if (err) {
                        return this._onError(err);
                    }
                    next();
                });
            };

            this._server.onConnect(this.session, err => {
                this._server.logger.info(
                    {
                        tnx: 'connection',
                        cid: this.id,
                        host: this.remoteAddress,
                        hostname: this.clientHostname
                    },
                    'Connection from %s',
                    this.clientHostname
                );

                if (err) {
                    this.send(err.responseCode || 554, err.message, false);
                    return this.close();
                }

                onSecureIfNeeded(() => {
                    this._ready = true; // Start accepting data from input

                    if (!this._server.options.useXClient && !this._server.options.useXForward) {
                        this.emitConnection();
                    }

                    this.send(
                        220,
                        this.name +
                            ' ' +
                            (this._server.options.lmtp ? 'LMTP' : 'ESMTP') +
                            (this._server.options.banner ? ' ' + this._server.options.banner : ''),
                        false
                    );

                    if (typeof next === 'function') {
                        next();
                    }
                });
            });
        };

        // Skip reverse name resolution if disabled.
        if (this._server.options.disableReverseLookup) {
            return reverseCb(null, false);
        }

        // also make sure that we do not wait too long over the reverse resolve call
        let greetingSent = false;
        let reverseTimer = setTimeout(() => {
            clearTimeout(reverseTimer);
            if (greetingSent) {
                return;
            }
            greetingSent = true;
            reverseCb(new Error('Timeout'));
        }, 1500);
        // Unref timer so DNS timeout doesn't prevent process exit
        reverseTimer.unref();

        // Helper function to handle resolver results consistently
        const handleResolverResult = (...args) => {
            clearTimeout(reverseTimer);
            if (greetingSent) {
                return;
            }
            greetingSent = true;
            reverseCb(...args);
        };

        try {
            // Use custom resolver if provided, otherwise use default dns.reverse
            if (this._server.options.resolver && typeof this._server.options.resolver.reverse === 'function') {
                this._server.options.resolver.reverse(this.remoteAddress.toString(), handleResolverResult);
            } else {
                // dns.reverse throws on invalid input, see https://github.com/nodejs/node/issues/3112
                dns.reverse(this.remoteAddress.toString(), handleResolverResult);
            }
        } catch (E) {
            clearTimeout(reverseTimer);
            if (greetingSent) {
                return;
            }
            greetingSent = true;
            reverseCb(E);
        }
    }

    /**
     * Send data to socket
     *
     * @param {Number} code Response code
     * @param {String|Array} data If data is Array, send a multi-line response
     * @param {String|Boolean} context Optional context for enhanced status codes
     */
    send(code, data, context) {
        let payload;
        let enhancedCode = this._getEnhancedStatusCode(code, context);

        if (Array.isArray(data)) {
            // Multi-line response - enhanced status code must appear on each line
            payload = data
                .map((line, i, arr) => {
                    let prefix = code + (i < arr.length - 1 ? '-' : ' ');
                    if (enhancedCode) {
                        prefix += enhancedCode + ' ';
                    }
                    return prefix + line;
                })
                .join('\r\n');
        } else {
            // Single line response
            let parts = [code];
            if (enhancedCode) {
                parts.push(enhancedCode);
            }
            if (data) {
                parts.push(data);
            }
            payload = parts.join(' ');
        }

        if (code >= 400) {
            this.session.error = payload;
        }

        // Ref. https://datatracker.ietf.org/doc/html/rfc4954#section-4
        if (code === 334 && payload === '334') {
            payload += ' ';
        }

        if (this._socket && !this._socket.destroyed && this._socket.readyState === 'open') {
            this._socket.write(payload + '\r\n');
            this._server.logger.debug(
                {
                    tnx: 'send',
                    cid: this.id,
                    user: (this.session.user && this.session.user.username) || this.session.user
                },
                'S:',
                payload
            );
        }

        if (code === 421) {
            this.close();
        }
    }

    /**
     * Close socket
     */
    close() {
        if (!this._socket.destroyed && this._socket.writable) {
            this._socket.end();
        }

        this._server.connections.delete(this);

        this._closing = true;
    }

    // PRIVATE METHODS

    /**
     * Setup socket event handlers
     */
    _setListeners(callback) {
        this._socket.on('close', hadError => this._onCloseEvent(hadError));
        this._socket.on('error', err => this._onError(err));
        this._socket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, () => this._onTimeout());
        this._socket.pipe(this._parser);
        if (!this.needsUpgrade) {
            return callback();
        }
        this.upgrade(() => false, callback);
    }

    _onCloseEvent(hadError) {
        this._server.logger.info(
            {
                tnx: 'close',
                cid: this.id,
                host: this.remoteAddress,
                user: (this.session.user && this.session.user.username) || this.session.user,
                hadError
            },
            '%s received "close" event from %s' + (hadError ? ' after error' : ''),
            this.id,
            this.remoteAddress
        );

        this._onClose();
    }

    /**
     * Fired when the socket is closed
     * @event
     */
    _onClose(/* hadError */) {
        if (this._parser) {
            this._parser.isClosed = true;
            this._socket.unpipe(this._parser);
            this._parser = false;
        }

        if (this._dataStream) {
            this._dataStream.unpipe();
            this._dataStream = null;
        }

        this._server.connections.delete(this);

        if (this._closed) {
            return;
        }

        this._closed = true;
        this._closing = false;

        this._server.logger.info(
            {
                tnx: 'close',
                cid: this.id,
                host: this.remoteAddress,
                user: (this.session.user && this.session.user.username) || this.session.user
            },
            'Connection closed to %s',
            this.clientHostname || this.remoteAddress
        );
        setImmediate(() => this._server.onClose(this.session));
    }

    /**
     * Fired when an error occurs with the socket
     *
     * @event
     * @param {Error} err Error object
     */
    _onError(err) {
        err.remote = this.remoteAddress;
        this._server.logger.error(
            {
                err,
                tnx: 'error',
                user: (this.session.user && this.session.user.username) || this.session.user
            },
            '%s %s %s',
            this.id,
            this.remoteAddress,
            err.message
        );

        if ((err.code === 'ECONNRESET' || err.code === 'EPIPE') && (!this.session.envelope || !this.session.envelope.mailFrom)) {
            // We got a connection error outside transaction. In most cases it means dirty
            // connection ending by the other party, so we can just ignore it
            this.close(); // mark connection as 'closing'
            return;
        }

        this.emit('error', err);
    }

    /**
     * Fired when socket timeouts. Closes connection
     *
     * @event
     */
    _onTimeout() {
        this.send(421, 'Timeout - closing connection');
    }

    /**
     * Checks if a selected command is available and invokes it
     *
     * @param {Buffer} command Single line of data from the client
     * @param {Function} callback Callback to run once the command is processed
     */
    _onCommand(command, callback) {
        let commandName = (command || '').toString().split(' ').shift().toUpperCase();
        this._server.logger.debug(
            {
                tnx: 'command',
                cid: this.id,
                command: commandName,
                user: (this.session.user && this.session.user.username) || this.session.user
            },
            'C:',
            (command || '').toString()
        );

        let handler;
        callback = callback || (() => false);

        // If server already closing then ignore commands
        if (this._server._closeTimeout) {
            return this.send(421, 'Server shutting down', commandName);
        }

        if (!this._ready) {
            // block spammers that send payloads before server greeting
            return this.send(421, this.name + ' You talk too soon', commandName);
        }

        // block malicious web pages that try to make SMTP calls from an AJAX request
        if (/^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) \/.* HTTP\/\d\.\d$/i.test(command)) {
            return this.send(421, 'HTTP requests not allowed', commandName);
        }

        if (this._upgrading) {
            // ignore any commands before TLS upgrade is finished
            return callback();
        }

        if (this._nextHandler) {
            // If we already have a handler method queued up then use this
            handler = this._nextHandler;
            this._nextHandler = false;
        } else {
            // detect handler from the command name
            switch (commandName) {
                case 'HELO':
                case 'EHLO':
                case 'LHLO':
                    this.openingCommand = commandName;
                    break;
            }
            if (this._server.options.lmtp) {
                switch (commandName) {
                    case 'HELO':
                    case 'EHLO':
                        this.send(500, 'Error: ' + commandName + ' not allowed in LMTP server', false);
                        return setImmediate(callback);
                    case 'LHLO':
                        commandName = 'EHLO';
                        break;
                }
            }
            if (this._isSupported(commandName)) {
                handler = this['handler_' + commandName];
            }
        }

        if (!handler) {
            // if the user makes more
            this._unrecognizedCommands++;
            if (this._unrecognizedCommands >= 10) {
                return this.send(421, 'Error: too many unrecognized commands', commandName);
            }

            this.send(500, 'Error: command not recognized', commandName);
            return setImmediate(callback);
        }

        // block users that try to fiddle around without logging in
        if (
            !this.session.user &&
            this._isSupported('AUTH') &&
            !this._server.options.authOptional &&
            commandName !== 'AUTH' &&
            this._maxAllowedUnauthenticatedCommands !== false
        ) {
            this._unauthenticatedCommands++;
            if (this._unauthenticatedCommands >= this._maxAllowedUnauthenticatedCommands) {
                return this.send(421, 'Error: too many unauthenticated commands', commandName);
            }
        }

        if (!this.hostNameAppearsAs && commandName && ['MAIL', 'RCPT', 'DATA', 'AUTH'].includes(commandName)) {
            this.send(503, 'Error: send ' + (this._server.options.lmtp ? 'LHLO' : 'HELO/EHLO') + ' first');
            return setImmediate(callback);
        }

        // Check if authentication is required
        if (!this.session.user && this._isSupported('AUTH') && ['MAIL', 'RCPT', 'DATA'].includes(commandName) && !this._server.options.authOptional) {
            this.send(
                530,
                typeof this._server.options.authRequiredMessage === 'string' ? this._server.options.authRequiredMessage : 'Error: authentication Required'
            );
            return setImmediate(callback);
        }

        handler.call(this, command, callback);
    }

    /**
     * Checks that a command is available and is not listed in the disabled commands array
     *
     * @param {String} command Command name
     * @returns {Boolean} Returns true if the command can be used
     */
    _isSupported(command) {
        command = (command || '').toString().trim().toUpperCase();
        return !this._server.options.disabledCommands.includes(command) && typeof this['handler_' + command] === 'function';
    }

    /**
     * Determines if enhanced status codes should be used
     * @returns {Boolean} True if enhanced status codes should be included in responses
     */
    _useEnhancedStatusCodes() {
        return !this._server.options.hideENHANCEDSTATUSCODES;
    }

    /**
     * Gets the appropriate enhanced status code for a given SMTP response code and context
     * @param {Number} code SMTP response code
     * @param {String|Boolean} context Optional context for more specific status codes
     * @returns {String} Enhanced status code or empty string if not applicable
     */
    _getEnhancedStatusCode(code, context) {
        if (context === false || !this._useEnhancedStatusCodes()) {
            return '';
        }

        // Skip 3xx responses as per RFC 2034
        if (code >= 300 && code < 400) {
            return '';
        }

        // Skip enhanced status codes for initial greeting and HELO/EHLO responses
        if (context && SKIPPED_COMMANDS_FOR_ENHANCED_STATUS_CODES.has(context)) {
            return '';
        }

        // Use contextual codes if available
        if (context && CONTEXTUAL_STATUS_CODES[context]) {
            return CONTEXTUAL_STATUS_CODES[context];
        }

        // Use default mapping
        if (ENHANCED_STATUS_CODES[code]) {
            return ENHANCED_STATUS_CODES[code];
        }

        // 2xx fallback
        if (code >= 200 && code < 300) {
            return '2.0.0';
        }

        // 4xx (transient failure)
        if (code >= 400 && code < 500) {
            return '4.0.0';
        }

        // 5xx (permanent failure)
        if (code >= 500) {
            return '5.0.0';
        }

        // safeguard (non-spec; but should never occur)
        return '';
    }

    /**
     * Parses commands like MAIL FROM and RCPT TO. Returns an object with the address and optional arguments.
     *
     * @param {[type]} name Address type, eg 'mail from' or 'rcpt to'
     * @param {[type]} command Data payload to parse
     * @returns {Object|Boolean} Parsed address in the form of {address:, args: {}} or false if parsing failed
     */
    _parseAddressCommand(name, command) {
        command = (command || '').toString();
        name = (name || '').toString().trim().toUpperCase();

        let parts = command.split(':');
        command = parts.shift().trim().toUpperCase();
        parts = parts.join(':').trim().split(/\s+/);

        let address = parts.shift();
        let args = false;
        let invalid = false;

        if (name !== command) {
            return false;
        }

        if (!/^<[^<>]*>$/.test(address)) {
            invalid = true;
        } else {
            address = address.substr(1, address.length - 2);
        }

        parts.forEach(part => {
            part = part.split('=');
            let key = part.shift().toUpperCase();
            let value = part.join('=') || true;

            // Skip parameters with empty keys
            if (!key || key.trim() === '') {
                return;
            }

            if (typeof value === 'string') {
                // decode 'xtext'
                value = value.replace(/\+([0-9A-F]{2})/g, (match, hex) => unescape('%' + hex));
            }

            if (!args) {
                args = {};
            }

            args[key] = value;
        });

        if (address) {
            // Validate email address format
            address = address.split('@');
            if (address.length !== 2 || !address[0] || !address[1]) {
                invalid = true;
            } else {
                let localPart = address[0];
                let domain = address[1];

                // Reasonable length limits to prevent abuse (more permissive than RFC 5321)
                // Note: Gmail and other providers may exceed RFC 5321's 64-char local-part limit
                if (localPart.length > 128 || domain.length > 255) {
                    invalid = true;
                } else {
                    // Validate local-part format - permissive validation
                    // Reject only clearly invalid patterns (dots at edges, consecutive dots)
                    // Allow common special characters used by email providers
                    if (localPart.startsWith('.') || localPart.endsWith('.') || localPart.includes('..')) {
                        invalid = true;
                    }

                    // Validate domain format (before punycode conversion)
                    // Reject clearly invalid patterns
                    if (
                        !invalid &&
                        (domain.startsWith('.') || domain.endsWith('.') || domain.includes('..') || domain.includes('.-') || domain.includes('-.') || !/^[a-zA-Z0-9\u0080-\uFFFF.-]+$/.test(domain))
                    ) {
                        invalid = true;
                    }
                }

                if (!invalid) {
                    try {
                        address = [localPart, '@', punycode.toUnicode(domain)].join('');
                    } catch (E) {
                        this._server.logger.error(
                            {
                                tnx: 'punycode',
                                cid: this.id,
                                user: (this.session.user && this.session.user.username) || this.session.user
                            },
                            'Failed to process punycode domain "%s". error=%s',
                            domain,
                            E.message
                        );
                        // If punycode conversion fails, treat as invalid
                        invalid = true;
                    }
                }
            }
        }

        return invalid
            ? false
            : {
                  address,
                  args
              };
    }

    /**
     * Resets or sets up a new session. We reuse existing session object to keep
     * application specific data.
     */
    _resetSession() {
        let session = this.session;

        // reset data that might be overwritten
        session.localAddress = this.localAddress;
        session.localPort = this.localPort;
        session.remoteAddress = this.remoteAddress;
        session.remotePort = this.remotePort;
        session.clientHostname = this.clientHostname;
        session.openingCommand = this.openingCommand;
        session.hostNameAppearsAs = this.hostNameAppearsAs;
        session.xClient = this._xClient;
        session.xForward = this._xForward;
        session.transmissionType = this._transmissionType();

        session.tlsOptions = this.tlsOptions;

        // reset transaction properties
        session.envelope = {
            mailFrom: false,
            rcptTo: [],
            /** @property {boolean} requireTLS - RFC 8689: Indicates client requires TLS for entire delivery chain */
            requireTLS: false,
            /** @property {string} bodyType - RFC 6152: Message body encoding type (7bit or 8bitmime) */
            bodyType: '7bit',
            /** @property {boolean} smtpUtf8 - RFC 6531: Indicates UTF-8 support is requested */
            smtpUtf8: false
        };

        if (!this._server.options.hideDSN)
            session.envelope.dsn = {
                ret: null, // RET parameter from MAIL FROM (FULL or HDRS)
                envid: null // ENVID parameter from MAIL FROM
            };

        session.transaction = this._transactionCounter + 1;
    }

    /**
     * Returns current transmission type
     *
     * @return {String} Transmission type
     */
    _transmissionType() {
        let type = this._server.options.lmtp ? 'LMTP' : 'SMTP';

        if (this.openingCommand === 'EHLO') {
            type = 'E' + type;
        }

        if (this.secure) {
            type += 'S';
        }

        if (this.session.user) {
            type += 'A';
        }

        return type;
    }

    emitConnection() {
        if (!this._canEmitConnection) {
            return;
        }
        this._canEmitConnection = false;
        this.emit('connect', {
            id: this.id,
            localAddress: this.localAddress,
            localPort: this.localPort,
            remoteAddress: this.remoteAddress,
            remotePort: this.remotePort,
            hostNameAppearsAs: this.hostNameAppearsAs,
            clientHostname: this.clientHostname
        });
    }

    // COMMAND HANDLERS

    /**
     * Processes EHLO. Requires valid hostname as the single argument.
     */
    handler_EHLO(command, callback) {
        let parts = command.toString().trim().split(/\s+/);
        let hostname = parts[1] || '';

        if (parts.length !== 2) {
            this.send(501, 'Error: syntax: ' + (this._server.options.lmtp ? 'LHLO' : 'EHLO') + ' hostname', false);
            return callback();
        }

        this.hostNameAppearsAs = hostname.toLowerCase();

        let features = ['PIPELINING', '8BITMIME', 'SMTPUTF8', 'ENHANCEDSTATUSCODES', 'DSN'].filter(feature => !this._server.options['hide' + feature]);

        if (this._server.options.authMethods.length && this._isSupported('AUTH') && !this.session.user) {
            features.push(['AUTH'].concat(this._server.options.authMethods).join(' '));
        }

        if (!this.secure && this._isSupported('STARTTLS') && !this._server.options.hideSTARTTLS) {
            features.push('STARTTLS');
        }

        if (this.secure && !this._server.options.hideREQUIRETLS) {
            features.push('REQUIRETLS');
        }

        if (this._server.options.size) {
            features.push('SIZE' + (this._server.options.hideSize ? '' : ' ' + this._server.options.size));
        }

        // XCLIENT ADDR removes any special privileges for the client
        if (!this._xClient.has('ADDR') && this._server.options.useXClient && this._isSupported('XCLIENT')) {
            features.push('XCLIENT NAME ADDR PORT PROTO HELO LOGIN');
        }

        // If client has already issued XCLIENT ADDR then it does not have privileges for XFORWARD anymore
        if (!this._xClient.has('ADDR') && this._server.options.useXForward && this._isSupported('XFORWARD')) {
            features.push('XFORWARD NAME ADDR PORT PROTO HELO IDENT SOURCE');
        }

        this._resetSession(); // EHLO is effectively the same as RSET

        // Format HELO response using configured format or default
        let heloResponse = this._server.options.heloResponse || '%s Nice to meet you, %s';
        let replacements = [this.name, this.clientHostname];
        let replacementIndex = 0;
        let formattedResponse = heloResponse.replace(/%s/g, () => replacements[replacementIndex++] || '');

        this.send(250, [formattedResponse].concat(features || []), false);

        callback();
    }

    /**
     * Processes HELO. Requires valid hostname as the single argument.
     */
    handler_HELO(command, callback) {
        let parts = command.toString().trim().split(/\s+/);
        let hostname = parts[1] || '';

        if (parts.length !== 2) {
            this.send(501, 'Error: Syntax: HELO hostname', false);
            return callback();
        }

        this.hostNameAppearsAs = hostname.toLowerCase();

        this._resetSession(); // HELO is effectively the same as RSET

        // Format HELO response using configured format or default
        let heloResponse = this._server.options.heloResponse || '%s Nice to meet you, %s';
        let replacements = [this.name, this.clientHostname];
        let replacementIndex = 0;
        let formattedResponse = heloResponse.replace(/%s/g, () => replacements[replacementIndex++] || '');

        this.send(250, formattedResponse, false);

        callback();
    }

    /**
     * Processes QUIT. Closes the connection
     */
    handler_QUIT(command, callback) {
        this.send(221, 'Bye');
        this.close();
        callback();
    }

    /**
     * Processes NOOP. Does nothing but keeps the connection alive
     */
    handler_NOOP(command, callback) {
        this.send(250, 'OK');
        callback();
    }

    /**
     * Processes RSET. Resets user and session info
     */
    handler_RSET(command, callback) {
        this._resetSession();

        this.send(250, 'Flushed');
        callback();
    }

    /**
     * Processes HELP. Responds with url to RFC
     */
    handler_HELP(command, callback) {
        this.send(214, 'See https://tools.ietf.org/html/rfc5321 for details');
        callback();
    }

    /**
     * Processes VRFY. Does not verify anything
     */
    handler_VRFY(command, callback) {
        this.send(252, 'Try to send something. No promises though');
        callback();
    }

    /**
     * Overrides connection info
     * http://www.postfix.org/XCLIENT_README.html
     *
     * TODO: add unit tests
     */
    handler_XCLIENT(command, callback) {
        // check if user is authorized to perform this command
        if (this._xClient.has('ADDR') || !this._server.options.useXClient) {
            this.send(550, 'Error: Not allowed');
            return callback();
        }

        // not allowed to change properties if already processing mail
        if (this.session.envelope.mailFrom) {
            this.send(503, 'Error: Mail transaction in progress');
            return callback();
        }

        let allowedKeys = ['NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN'];
        let parts = command.toString().trim().split(/\s+/);
        let key, value;
        let data = new Map();
        parts.shift(); // remove XCLIENT prefix

        if (!parts.length) {
            this.send(501, 'Error: Bad command parameter syntax');
            return callback();
        }

        let loginValue = false;

        // parse and validate arguments
        for (let i = 0, len = parts.length; i < len; i++) {
            value = parts[i].split('=');
            key = value.shift();
            if (value.length !== 1 || !allowedKeys.includes(key.toUpperCase())) {
                this.send(501, 'Error: Bad command parameter syntax');
                return callback();
            }
            key = key.toUpperCase();

            // value is xtext
            value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, (match, hex) => unescape('%' + hex));

            if (['[UNAVAILABLE]', '[TEMPUNAVAIL]'].includes(value.toUpperCase())) {
                value = false;
            }

            if (data.has(key)) {
                // ignore duplicate keys
                continue;
            }

            data.set(key, value);

            switch (key) {
                // handled outside the switch
                case 'LOGIN':
                    loginValue = value;
                    break;
                case 'ADDR':
                    if (value) {
                        value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:"

                        if (!net.isIP(value)) {
                            this.send(501, 'Error: Bad command parameter syntax. Invalid address');
                            return callback();
                        }

                        if (net.isIPv6(value)) {
                            value = ipv6normalize(value);
                        }

                        this._server.logger.info(
                            {
                                tnx: 'xclient',
                                cid: this.id,
                                xclientKey: 'ADDR',
                                xclient: value,
                                user: (this.session.user && this.session.user.username) || this.session.user
                            },
                            'XCLIENT from %s through %s',
                            value,
                            this.remoteAddress
                        );

                        // store original value for reference as ADDR:DEFAULT
                        if (!this._xClient.has('ADDR:DEFAULT')) {
                            this._xClient.set('ADDR:DEFAULT', this.remoteAddress);
                        }

                        this.remoteAddress = value;
                        this.hostNameAppearsAs = false; // reset client provided hostname, require HELO/EHLO
                    }
                    break;
                case 'NAME':
                    value = value || '';
                    this._server.logger.info(
                        {
                            tnx: 'xclient',
                            cid: this.id,
                            xclientKey: 'NAME',
                            xclient: value,
                            user: (this.session.user && this.session.user.username) || this.session.user
                        },
                        'XCLIENT hostname resolved as "%s"',
                        value
                    );

                    // store original value for reference as NAME:DEFAULT
                    if (!this._xClient.has('NAME:DEFAULT')) {
                        this._xClient.set('NAME:DEFAULT', this.clientHostname || '');
                    }

                    this.clientHostname = value.toLowerCase();
                    break;
                case 'PORT':
                    value = Number(value) || '';
                    this._server.logger.info(
                        {
                            tnx: 'xclient',
                            cid: this.id,
                            xclientKey: 'PORT',
                            xclient: value,
                            user: (this.session.user && this.session.user.username) || this.session.user
                        },
                        'XCLIENT remote port resolved as "%s"',
                        value
                    );

                    // store original value for reference as NAME:DEFAULT
                    if (!this._xClient.has('PORT:DEFAULT')) {
                        this._xClient.set('PORT:DEFAULT', this.remotePort || '');
                    }

                    this.remotePort = value;
                    break;
                default:
                // other values are not relevant
            }
            this._xClient.set(key, value);
        }

        let checkLogin = done => {
            if (typeof loginValue !== 'string') {
                return done();
            }
            if (!loginValue) {
                // clear authentication session?
                this._server.logger.info(
                    {
                        tnx: 'deauth',
                        cid: this.id,
                        user: (this.session.user && this.session.user.username) || this.session.user
                    },
                    'User deauthenticated using %s',
                    'XCLIENT'
                );
                this.session.user = false;
                return done();
            }
            let method = 'SASL_XCLIENT';
            sasl[method].call(this, [loginValue], err => {
                if (err) {
                    this.send(550, err.message);
                    this.close();
                    return;
                }
                done();
            });
        };

        // Use [ADDR] if NAME was empty
        if (this.remoteAddress && !this.clientHostname) {
            this.clientHostname = '[' + this.remoteAddress + ']';
        }

        if (data.has('ADDR')) {
            this.emitConnection();
        }

        checkLogin(() => {
            // success
            this.send(
                220,
                this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : '')
            );
            callback();
        });
    }

    /**
     * Processes XFORWARD data
     * http://www.postfix.org/XFORWARD_README.html
     *
     * TODO: add unit tests
     */
    handler_XFORWARD(command, callback) {
        // check if user is authorized to perform this command
        if (!this._server.options.useXForward) {
            this.send(550, 'Error: Not allowed');
            return callback();
        }

        // not allowed to change properties if already processing mail
        if (this.session.envelope.mailFrom) {
            this.send(503, 'Error: Mail transaction in progress');
            return callback();
        }

        let allowedKeys = ['NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'IDENT', 'SOURCE'];
        let parts = command.toString().trim().split(/\s+/);
        let key, value;
        let data = new Map();
        let hasAddr = false;
        parts.shift(); // remove XFORWARD prefix

        if (!parts.length) {
            this.send(501, 'Error: Bad command parameter syntax');
            return callback();
        }

        // parse and validate arguments
        for (let i = 0, len = parts.length; i < len; i++) {
            value = parts[i].split('=');
            key = value.shift();
            if (value.length !== 1 || !allowedKeys.includes(key.toUpperCase())) {
                this.send(501, 'Error: Bad command parameter syntax');
                return callback();
            }
            key = key.toUpperCase();
            if (data.has(key)) {
                // ignore duplicate keys
                continue;
            }

            // value is xtext
            value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, (match, hex) => unescape('%' + hex));

            if (value.toUpperCase() === '[UNAVAILABLE]') {
                value = false;
            }

            data.set(key, value);

            switch (key) {
                case 'ADDR':
                    if (value) {
                        value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:"

                        if (!net.isIP(value)) {
                            this.send(501, 'Error: Bad command parameter syntax. Invalid address');
                            return callback();
                        }

                        if (net.isIPv6(value)) {
                            value = ipv6normalize(value);
                        }

                        this._server.logger.info(
                            {
                                tnx: 'xforward',
                                cid: this.id,
                                xforwardKey: 'ADDR',
                                xforward: value,
                                user: (this.session.user && this.session.user.username) || this.session.user
                            },
                            'XFORWARD from %s through %s',
                            value,
                            this.remoteAddress
                        );

                        // store original value for reference as ADDR:DEFAULT
                        if (!this._xClient.has('ADDR:DEFAULT')) {
                            this._xClient.set('ADDR:DEFAULT', this.remoteAddress);
                        }

                        hasAddr = true;
                        this.remoteAddress = value;
                    }
                    break;
                case 'NAME':
                    value = value || '';
                    this._server.logger.info(
                        {
                            tnx: 'xforward',
                            cid: this.id,
                            xforwardKey: 'NAME',
                            xforward: value,
                            user: (this.session.user && this.session.user.username) || this.session.user
                        },
                        'XFORWARD hostname resolved as "%s"',
                        value
                    );
                    this.clientHostname = value.toLowerCase();
                    break;
                case 'PORT':
                    value = Number(value) || 0;
                    this._server.logger.info(
                        {
                            tnx: 'xforward',
                            cid: this.id,
                            xforwardKey: 'PORT',
                            xforward: value,
                            user: (this.session.user && this.session.user.username) || this.session.user
                        },
                        'XFORWARD port resolved as "%s"',
                        value
                    );
                    this.remotePort = value;
                    break;
                case 'HELO':
                    value = (value || '').toString().toLowerCase();
                    this._server.logger.info(
                        {
                            tnx: 'xforward',
                            cid: this.id,
                            xforwardKey: 'HELO',
                            xforward: value,
                            user: (this.session.user && this.session.user.username) || this.session.user
                        },
                        'XFORWARD HELO name resolved as "%s"',
                        value
                    );
                    this.hostNameAppearsAs = value;
                    break;
                default:
                // other values are not relevant
            }
            this._xForward.set(key, value);
        }

        if (hasAddr) {
            this._canEmitConnection = true;
            this.emitConnection();
        }

        // success
        this.send(250, 'OK');
        callback();
    }

    /**
     * Upgrades connection to TLS if possible
     */
    handler_STARTTLS(command, callback) {
        if (this.secure) {
            this.send(503, 'Error: TLS already active');
            return callback();
        }

        this.send(220, 'Ready to start TLS');

        this.upgrade(callback);
    }

    /**
     * Check if selected authentication is available and delegate auth data to SASL
     */
    handler_AUTH(command, callback) {
        let args = command.toString().trim().split(/\s+/);
        let method;
        let handler;

        args.shift(); // remove AUTH
        method = (args.shift() || '').toString().toUpperCase(); // get METHOD and keep additional arguments in the array
        handler = sasl['SASL_' + method];
        handler = handler ? handler.bind(this) : handler;

        if (!this.secure && this._isSupported('STARTTLS') && !this._server.options.hideSTARTTLS && !this._server.options.allowInsecureAuth) {
            this.send(538, 'Error: Must issue a STARTTLS command first');
            return callback();
        }

        if (this.session.user) {
            this.send(503, 'Error: No identity changes permitted');
            return callback();
        }

        if (!this._server.options.authMethods.includes(method) || typeof handler !== 'function') {
            this.send(504, 'Error: Unrecognized authentication type');
            return callback();
        }

        handler(args, callback);
    }

    /**
     * Validates MAIL FROM parameters
     * @param {Object} parsed - Parsed address command with args
     * @returns {Object|null} Returns error object {code, message, enhancedCode} if validation fails, null if successful
     */
    _validateMailParams(parsed) {
        // Validate BODY parameter (RFC 6152 - 8BITMIME)
        // Note: BINARYMIME is not supported as it requires BDAT command (RFC 3030)
        if (parsed.args.BODY) {
            const bodyType = parsed.args.BODY.toLowerCase();
            if (bodyType !== '7bit' && bodyType !== '8bitmime') {
                return {
                    code: 501,
                    message: 'Invalid BODY parameter value. Must be 7BIT or 8BITMIME',
                    enhancedCode: null
                };
            }
        }

        // Validate SMTPUTF8 parameter (RFC 6531)
        if (parsed.args.SMTPUTF8 !== undefined) {
            if (parsed.args.SMTPUTF8 !== true) {
                return {
                    code: 501,
                    message: 'Invalid SMTPUTF8 parameter. This flag does not accept a value',
                    enhancedCode: null
                };
            }
        }

        // Validate REQUIRETLS parameter (RFC 8689)
        if (parsed.args.REQUIRETLS !== undefined) {
            if (!this.secure) {
                return {
                    code: 530,
                    message: 'REQUIRETLS not permitted on non-TLS connections',
                    enhancedCode: null
                };
            }
            if (parsed.args.REQUIRETLS !== true) {
                return {
                    code: 501,
                    message: 'Invalid REQUIRETLS parameter. This flag does not accept a value',
                    enhancedCode: null
                };
            }
        }

        // Validate DSN parameters if DSN is supported
        if (!this._server.options.hideDSN) {
            if (parsed.args.RET) {
                const ret = parsed.args.RET.toUpperCase();
                if (ret !== 'FULL' && ret !== 'HDRS') {
                    return {
                        code: 501,
                        message: 'Invalid RET parameter value. Must be FULL or HDRS',
                        enhancedCode: null
                    };
                }
            }
        }

        return null; // All validations passed
    }

    /**
     * Applies validated MAIL FROM parameters to session envelope
     * @param {Object} parsed - Parsed address command with args
     */
    _applyMailParams(parsed) {
        // Apply BODY parameter
        if (parsed.args.BODY) {
            this.session.envelope.bodyType = parsed.args.BODY.toLowerCase();
        }

        // Apply SMTPUTF8 parameter
        if (parsed.args.SMTPUTF8 === true) {
            this.session.envelope.smtpUtf8 = true;
        }

        // Apply REQUIRETLS parameter
        if (parsed.args.REQUIRETLS === true) {
            this.session.envelope.requireTLS = true;
        }

        // Apply DSN parameters
        if (!this._server.options.hideDSN) {
            if (parsed.args.RET) {
                this.session.envelope.dsn.ret = parsed.args.RET.toUpperCase();
            }
            if (parsed.args.ENVID) {
                this.session.envelope.dsn.envid = parsed.args.ENVID;
            }
        }
    }

    /**
     * Processes MAIL FROM command, parses address and extra arguments
     */
    handler_MAIL(command, callback) {
        let parsed = this._parseAddressCommand('mail from', command);

        // in case we still haven't informed about the new connection emit it
        this.emitConnection();

        // sender address can be empty, so we only check if parsing failed or not
        if (!parsed) {
            this.send(501, 'Error: Bad sender address syntax', 'MAILBOX_SYNTAX_ERROR');
            return callback();
        }

        if (this.session.envelope.mailFrom) {
            this.send(503, 'Error: nested MAIL command');
            return callback();
        }

        if (!this._server.options.hideSize && this._server.options.size && parsed.args.SIZE && Number(parsed.args.SIZE) > this._server.options.size) {
            this.send(552, 'Error: message exceeds fixed maximum message size ' + this._server.options.size, 'SYSTEM_FULL');
            return callback();
        }

        // Validate all MAIL FROM parameters
        const validationError = this._validateMailParams(parsed);
        if (validationError) {
            this.send(validationError.code, validationError.message, validationError.enhancedCode);
            return callback();
        }

        // Apply validated parameters to session envelope before calling onMailFrom
        // so the handler can access them
        this._applyMailParams(parsed);

        this._server.onMailFrom(parsed, this.session, err => {
            if (err) {
                this.send(err.responseCode || 550, err.message);
                return callback();
            }

            this.session.envelope.mailFrom = parsed;

            this.send(250, 'Accepted', 'MAIL_FROM_OK');
            callback();
        });
    }

    /**
     * Processes RCPT TO command, parses address and extra arguments
     */
    handler_RCPT(command, callback) {
        let parsed = this._parseAddressCommand('rcpt to', command);

        // recipient address can not be empty
        if (!parsed || !parsed.address) {
            this.send(501, 'Error: Bad recipient address syntax', 'MAILBOX_SYNTAX_ERROR');
            return callback();
        }

        if (!this.session.envelope.mailFrom) {
            this.send(503, 'Error: need MAIL command');
            return callback();
        }

        // Process DSN parameters if DSN is supported
        if (!this._server.options.hideDSN) {
            // Validate NOTIFY parameter
            if (parsed.args.NOTIFY) {
                const notify = parsed.args.NOTIFY.toUpperCase();
                const validNotifyValues = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
                const notifyValues = notify.split(',');

                for (let value of notifyValues) {
                    if (!validNotifyValues.includes(value)) {
                        this.send(501, 'Error: NOTIFY parameter must be NEVER, SUCCESS, FAILURE, or DELAY');
                        return callback();
                    }
                }

                // NEVER cannot be combined with other values
                if (notifyValues.includes('NEVER') && notifyValues.length > 1) {
                    this.send(501, 'Error: NOTIFY=NEVER cannot be combined with other values');
                    return callback();
                }

                parsed.dsn = parsed.dsn || {};
                parsed.dsn.notify = notifyValues;
            }

            // Store ORCPT parameter
            if (parsed.args.ORCPT) {
                parsed.dsn = parsed.dsn || {};
                parsed.dsn.orcpt = parsed.args.ORCPT;
            }
        }

        this._server.onRcptTo(parsed, this.session, err => {
            if (err) {
                this.send(err.responseCode || 550, err.message);
                return callback();
            }

            // check if the address is already used, if so then overwrite
            for (let i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
                if (this.session.envelope.rcptTo[i].address.toLowerCase() === parsed.address.toLowerCase()) {
                    this.session.envelope.rcptTo[i] = parsed;
                    parsed = false;
                    break;
                }
            }

            if (parsed) {
                this.session.envelope.rcptTo.push(parsed);
            }

            this.send(250, 'Accepted', 'RCPT_TO_OK');
            callback();
        });
    }

    /**
     * Processes DATA by forwarding incoming stream to the onData handler
     */
    handler_DATA(command, callback) {
        if (!this.session.envelope.rcptTo.length) {
            this.send(503, 'Error: need RCPT command');
            return callback();
        }

        if (!this._parser) {
            return callback();
        }

        this._dataStream = this._parser.startDataMode(this._server.options.size);

        let close = (err, message) => {
            let i, len;

            this._server.logger.debug(
                {
                    tnx: 'data',
                    cid: this.id,
                    bytes: this._parser.dataBytes,
                    user: (this.session.user && this.session.user.username) || this.session.user
                },
                'C: <%s bytes of DATA>',
                this._parser.dataBytes
            );

            if (typeof this._dataStream === 'object' && this._dataStream && this._dataStream.readable) {
                this._dataStream.removeAllListeners();
            }

            if (err) {
                if (this._server.options.lmtp) {
                    // separate error response for every recipient when using LMTP
                    for (i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
                        this.send(err.responseCode || 450, err.message);
                    }
                } else {
                    // single error response when using SMTP
                    this.send(err.responseCode || 450, err.message);
                }
            } else if (Array.isArray(message)) {
                // separate responses for every recipient when using LMTP
                message.forEach(response => {
                    if (/Error\]$/i.test(Object.prototype.toString.call(response))) {
                        this.send(response.responseCode || 450, response.message);
                    } else {
                        this.send(250, typeof response === 'string' ? response : 'OK: message accepted', 'DATA_OK');
                    }
                });
            } else if (this._server.options.lmtp) {
                // separate success response for every recipient when using LMTP
                for (i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
                    this.send(250, typeof message === 'string' ? message : 'OK: message accepted', 'DATA_OK');
                }
            } else {
                // single success response when using SMTP
                this.send(250, typeof message === 'string' ? message : 'OK: message queued', 'DATA_OK');
            }

            this._transactionCounter++;

            this._unrecognizedCommands = 0; // reset unrecognized commands counter
            this._resetSession(); // reset session state

            if (typeof this._parser === 'object' && this._parser) {
                this._parser.continue();
            }
        };

        this._server.onData(this._dataStream, this.session, (err, message) => {
            // ensure _dataStream is an object and not set to null by premature closing
            // do not continue until the stream has actually ended
            if (typeof this._dataStream === 'object' && this._dataStream && this._dataStream.readable) {
                this._dataStream.on('end', () => close(err, message));
                return;
            }
            close(err, message);
        });

        this.send(354, 'End data with <CR><LF>.<CR><LF>');
        callback();
    }

    // Dummy handlers for some old sendmail specific commands

    /**
     * Processes sendmail WIZ command, upgrades to "wizard mode"
     */
    handler_WIZ(command, callback) {
        let args = command.toString().trim().split(/\s+/);
        let password;

        args.shift(); // remove WIZ
        password = (args.shift() || '').toString();

        // require password argument
        if (!password) {
            this.send(500, 'You are no wizard!');
            return callback();
        }

        // all passwords pass validation, so everyone is a wizard!
        this.session.isWizard = true;
        this.send(200, 'Please pass, oh mighty wizard');
        callback();
    }

    /**
     * Processes sendmail SHELL command, should return interactive shell but this is a dummy function
     * so no actual shell is provided to the client
     */
    handler_SHELL(command, callback) {
        this._server.logger.info(
            {
                tnx: 'shell',
                cid: this.id,
                user: (this.session.user && this.session.user.username) || this.session.user
            },
            'Client tried to invoke SHELL'
        );

        if (!this.session.isWizard) {
            this.send(500, 'Mere mortals must not mutter that mantra');
            return callback();
        }

        this.send(500, 'Error: Invoking shell is not allowed. This incident will be reported.');
        callback();
    }

    /**
     * Processes sendmail KILL command
     */
    handler_KILL(command, callback) {
        this._server.logger.info(
            {
                tnx: 'kill',
                cid: this.id,
                user: (this.session.user && this.session.user.username) || this.session.user
            },
            'Client tried to invoke KILL'
        );

        this.send(500, 'Can not kill Mom');
        callback();
    }

    upgrade(callback, secureCallback) {
        this._socket.unpipe(this._parser);
        this._upgrading = true;
        setImmediate(callback); // resume input stream

        let secureContext = this._server.secureContext.get('*');
        let socketOptions = {
            secureContext,
            isServer: true,
            server: this._server.server,

            SNICallback: this._server.options.SNICallback
        };

        // Apply additional socket options if these are set in the server options
        ['requestCert', 'rejectUnauthorized', 'NPNProtocols', 'SNICallback', 'session', 'requestOCSP'].forEach(key => {
            if (key in this._server.options) {
                socketOptions[key] = this._server.options[key];
            }
        });

        // remove all listeners from the original socket besides the error handler
        this._socket.removeAllListeners();
        this._socket.on('error', err => this._onError(err));

        // upgrade connection
        let secureSocket = new tls.TLSSocket(this._socket, socketOptions);

        secureSocket.once('close', hadError => this._onCloseEvent(hadError));
        secureSocket.once('error', err => this._onError(err));
        secureSocket.once('_tlsError', err => this._onError(err));
        secureSocket.once('clientError', err => this._onError(err));

        secureSocket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, () => this._onTimeout());

        secureSocket.on('secure', () => {
            this.session.secure = this.secure = true;
            this._socket = secureSocket;
            this._upgrading = false;

            this.session.tlsOptions = this.tlsOptions = this._socket.getCipher();
            this.session.servername = this._socket.servername;
            let cipher = this.session.tlsOptions && this.session.tlsOptions.name;
            this._server.logger.info(
                {
                    tnx: 'starttls',
                    cid: this.id,
                    user: (this.session.user && this.session.user.username) || this.session.user,
                    cipher
                },
                'Connection upgraded to TLS using',
                cipher || 'N/A'
            );
            this._server.onSecure(this._socket, this.session, err => {
                if (err) {
                    return this._onError(err);
                }
                this._socket.pipe(this._parser);
                if (typeof secureCallback === 'function') {
                    secureCallback();
                }
            });
        });
    }
}
// Expose to the world
module.exports.SMTPConnection = SMTPConnection;
