IceAgent.prototype.bindingSuccess = function (socket, packet, rinfo) {
  var candidatePair =
    this.checklist.pendingRequestTransactions[packet.transactionID];
  delete this.checklist.pendingRequestTransactions[packet.transactionID];

  if (!candidatePair) {
    // This usually occurs once for the external STUN server we first spoke to,
    // ie. stun.l.google.com
    console.warn('binding success for unknown transactionID');
    return;
  }

  var dest = Packet.getAttribute(packet).xorMappedAddress;
  if (net.isIPv6(dest.host)) {
    dest.host = normalizeIPv6(dest.host);
  }

  // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2
  if (matchingSourceDest(rinfo, dest, candidatePair)) {
    // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.3
    candidatePair.succeed();
    this.checklist.unfreezeAll();
  } else {
    // TODO: Issue #52
    // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.1
    // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2.1.3
    console.warn('Found a peer reflexive candidate');
    debug.printPeerReflexive(rinfo, dest, candidatePair);
  }

  // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.2
  // TODO: there's more to do here
  candidatePair.valid = true;
  this.checklist.validList.push(candidatePair);
  // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.3
  this.checklist.checkForFailure();
};
SMTPConnection.prototype.handler_XFORWARD = function (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();
    }

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

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

    // parse and validate arguments
    for (var i = 0, len = parts.length; i < len; i++) {
        value = parts[i].split('=');
        key = value.shift();
        if (value.length !== 1 || allowedKeys.indexOf(key.toUpperCase()) < 0) {
            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, function (match, hex) {
            return 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('[%s] XFORWARD from %s through %s', this._id, value, this.remoteAddress);
                }
                break;
            case 'NAME':
                value = value || '';
                this._server.logger.info('[%s] XFORWARD hostname resolved as "%s"', this._id, value);
                break;
            default:
                // other values are not relevant
        }
        this._xForward.set(key, value);
    }

    // success
    this.send(250, 'OK');
    callback();
};
SMTPConnection.prototype.handler_XCLIENT = function (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();
    }

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

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

    // parse and validate arguments
    for (var i = 0, len = parts.length; i < len; i++) {
        value = parts[i].split('=');
        key = value.shift();
        if (value.length !== 1 || allowedKeys.indexOf(key.toUpperCase()) < 0) {
            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, function (match, hex) {
            return unescape('%' + hex);
        });

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

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

        data.set(key, value);

        switch (key) {
            case 'LOGIN':
                if (!value) {
                    if (this.session.user) {
                        this._server.logger.info('[%s] User deauthenticated using %s', this._id, 'XCLIENT');
                    }
                } else {
                    this._server.logger.info('[%s] %s authenticated using %s', this._id, value, 'XCLIENT');
                    this.session.user = {
                        username: 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('[%s] XCLIENT from %s through %s', this._id, 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('[%s] XCLIENT hostname resolved as "%s"', this._id, 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;
            default:
                // other values are not relevant
        }
        this._xClient.set(key, value);
    }

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

    // success
    this.send(220, this.name + ' ESMTP' + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
    callback();
};
/**
 * Creates a handler for new socket
 *
 * @constructor
 * @param {Object} server Server instance
 * @param {Object} socket Socket instance
 */
function SMTPConnection(server, socket) {
    EventEmitter.call(this);

    // Random session ID, used for logging
    this._id = crypto.randomBytes(9).toString('base64');

    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 = this._onCommand.bind(this);

    // 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.secure = !!this._server.options.secure;

    // Store remote address for later usage
    this.remoteAddress = this._socket.remoteAddress;

    // normalize IPv6 addresses
    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;

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

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