%PDF- %PDF-
Direktori : /home/opti/workflow/node_modules/simplesmtp/lib/ |
Current File : /home/opti/workflow/node_modules/simplesmtp/lib/server.js |
'use strict'; /** * @fileOverview This is the main file for the simplesmtp library to create custom SMTP servers * @author <a href='mailto:andris@node.ee'>Andris Reinman</a> */ var RAIServer = require('rai').RAIServer, EventEmitter = require('events').EventEmitter, oslib = require('os'), utillib = require('util'), dnslib = require('dns'), crypto = require('crypto'); // expose to the world module.exports = function(options) { return new SMTPServer(options); }; /** * <p>Constructs a SMTP server</p> * * <p>Possible options are:</p> * * <ul> * <li><b>name</b> - the hostname of the server, will be used for * informational messages</li> * <li><b>debug</b> - if set to true, print out messages about the connection</li> * <li><b>timeout</b> - client timeout in milliseconds, defaults to 60 000</li> * <li><b>secureConnection</b> - start a server on secure connection</li> * <li><b>SMTPBanner</b> - greeting banner that is sent to the client on connection</li> * <li><b>requireAuthentication</b> - if set to true, require that the client * must authenticate itself</li> * <li><b>enableAuthentication</b> - if set to true, client may authenticate itself but don't have to</li> * <li><b>maxSize</b> - maximum size of an e-mail in bytes</li> * <li><b>credentials</b> - TLS credentials</li> * <li><b>authMethods</b> - allowed authentication methods, defaults to <code>['PLAIN', 'LOGIN']</code></li> * <li><b>disableEHLO</b> - if set, support HELO only</li> * <li><b>ignoreTLS</b> - if set, allow client do not use STARTTLS</li> * <li><b>disableDNSValidation</b> - if set, do not validate sender domains</li> * <li><b>maxClients</b> - if set, limit the number of simultaneous connections to the server</li> * </ul> * * @constructor * @namespace SMTP Server module * @param {Object} [options] Options object */ function SMTPServer(options) { EventEmitter.call(this); this.connectedClients = 0; this.options = options || {}; this.options.name = this.options.name || (oslib.hostname && oslib.hostname()) || (oslib.getHostname && oslib.getHostname()) || '127.0.0.1'; this.options.authMethods = (this.options.authMethods || ['PLAIN', 'LOGIN']).map( function(auth) { return auth.toUpperCase().trim(); }); this.options.disableEHLO = !! this.options.disableEHLO; this.options.ignoreTLS = !! this.options.ignoreTLS; this.SMTPServer = new RAIServer({ secureConnection: !! this.options.secureConnection, credentials: this.options.credentials, timeout: this.options.timeout || 60 * 1000, disconnectOnTimeout: false, debug: !! this.options.debug }); this.SMTPServer.on('connect', this._createSMTPServerConnection.bind(this)); } utillib.inherits(SMTPServer, EventEmitter); /** * Server starts listening on defined port and hostname * * @param {Number} port The port number to listen * @param {String} [host] The hostname to listen * @param {Function} callback The callback function to run when the server is listening */ SMTPServer.prototype.listen = function(port, host, callback) { this.SMTPServer.listen(port, host, callback); }; /** * <p>Closes the server</p> * * @param {Function} callback The callback function to run when the server is closed */ SMTPServer.prototype.end = function(callback) { this.SMTPServer.end(callback); }; /** * <p>Creates a new {@link SMTPServerConnection} object and links the main server with * the client socket</p> * * @param {Object} client RAISocket object to a client */ SMTPServer.prototype._createSMTPServerConnection = function(client) { new SMTPServerConnection(this, client); }; /** * <p>Sets up a handler for the connected client</p> * * <p>Restarts the state and sets up event listeners for client actions</p> * * @constructor * @param {Object} server {@link SMTPServer} instance * @param {Object} client RAISocket instance for the client */ function SMTPServerConnection(server, client) { this.server = server; this.client = client; this.init(); this.server.connectedClients++; if (!this.client.remoteAddress) { if (this.server.options.debug) { console.log('Client already disconnected'); } this.client.end(); return; } if (this.server.options.debug) { console.log('Connection from', this.client.remoteAddress); } this.client.on('timeout', this._onTimeout.bind(this)); this.client.on('error', this._onError.bind(this)); this.client.on('command', this._onCommand.bind(this)); this.client.on('end', this._onEnd.bind(this)); this.client.on('data', this._onData.bind(this)); this.client.on('ready', this._onDataReady.bind(this)); // Too many clients. Disallow processing if (this.server.options.maxClients && this.server.connectedClients > this.server.options.maxClients) { this.end('421 ' + this.server.options.name + ' ESMTP - Too many connections. Please try again later.'); } else { // Send the greeting banner. Force ESMTP notice this.client.send('220 ' + this.server.options.name + ' ESMTP ' + (this.server.options.SMTPBanner || 'node.js simplesmtp')); } } /** * <p>Reset the envelope state</p> * * <p>If <code>keepAuthData</code> is set to true, then doesn't remove * authentication data</p> * * @param {Boolean} [keepAuthData=false] If set to true keep authentication data */ SMTPServerConnection.prototype.init = function(keepAuthData) { if (this.envelope === undefined) { this.envelope = {}; } this.envelope.from = ''; this.envelope.to = []; this.envelope.date = new Date(); if (this.hostNameAppearsAs) { this.envelope.host = this.hostNameAppearsAs; } if (this.client.remoteAddress) { this.envelope.remoteAddress = this.client.remoteAddress; } if (!keepAuthData) { this.authentication = { username: false, authenticated: false, state: 'NORMAL' }; } this.envelope.authentication = this.authentication; }; /** * <p>Sends a message to the client and closes the connection</p> * * @param {String} [message] if set, send it to the client before disconnecting */ SMTPServerConnection.prototype.end = function(message) { if (message) { this.client.send(message); } this.client.end(); }; /** * <p>Will be called when the connection to the client is closed</p> * * @event */ SMTPServerConnection.prototype._onEnd = function() { if (this.server.options.debug) { console.log('Connection closed to', this.client.remoteAddress); } this.server.connectedClients--; try { this.client.end(); } catch (E) {} this.server.emit('close', this.envelope); }; /** * <p>Will be called when timeout occurs</p> * * @event */ SMTPServerConnection.prototype._onTimeout = function() { this.end('421 4.4.2 ' + this.server.options.name + ' Error: timeout exceeded'); }; /** * <p>Will be called when an error occurs</p> * * @event */ SMTPServerConnection.prototype._onError = function() { this.end('421 4.4.2 ' + this.server.options.name + ' Error: client error'); }; /** * <p>Will be called when a command is received from the client</p> * * <p>If there's curently an authentication process going on, route * the data to <code>_handleAuthLogin</code>, otherwise act as * defined</p> * * @event * @param {String} command Command * @param {Buffer} command Payload related to the command */ SMTPServerConnection.prototype._onCommand = function(command, payload) { if (this.authentication.state == 'AUTHPLAINUSERDATA') { this._handleAuthPlain(command.toString('utf-8').trim().split(' ')); return; } if (this.authentication.state == 'AUTHENTICATING') { this._handleAuthLogin(command); return; } if (this.authentication.state == 'AUTHXOAUTH2') { this._handleAuthXOAuth2(command); return; } switch ((command || '').toString().trim().toUpperCase()) { // Should not occur too often case 'HELO': this._onCommandHELO(payload.toString('utf-8').trim()); break; // Lists server capabilities case 'EHLO': if (!this.server.options.disableEHLO) { this._onCommandEHLO(payload.toString('utf-8').trim()); } else { this.client.send('502 5.5.2 Error: command not recognized'); } break; // Closes the connection case 'QUIT': this.end('221 2.0.0 Goodbye!'); break; // Resets the current state case 'RSET': this._onCommandRSET(); break; // Doesn't work for spam related purposes case 'VRFY': this.client.send('252 2.1.5 Send some mail, I\'ll try my best'); break; // Initiate an e-mail by defining a sender case 'MAIL': this._onCommandMAIL(payload.toString('utf-8').trim()); break; // Add recipients to the e-mail envelope case 'RCPT': this._onCommandRCPT(payload.toString('utf-8').trim()); break; // Authenticate if needed case 'AUTH': this._onCommandAUTH(payload); break; // Start accepting binary data stream case 'DATA': this._onCommandDATA(); break; // Upgrade connection to secure TLS case 'STARTTLS': this._onCommandSTARTTLS(); break; // No operation case 'NOOP': this._onCommandNOOP(); break; // No operation case '': // ignore blank lines break; // Display an error on anything else default: this.client.send('502 5.5.2 Error: command not recognized'); } }; /** * <p>Initiate an e-mail by defining a sender.</p> * * <p>This doesn't work if authorization is required but the client is * not logged in yet.</p> * * <p>If <code>validateSender</code> option is set to true, then emits * <code>'validateSender'</code> and wait for the callback before moving * on</p> * * @param {String} mail Address payload in the form of 'FROM:<address>' */ SMTPServerConnection.prototype._onCommandMAIL = function(mail) { var self = this, match, email, domain; if (!this.hostNameAppearsAs) { return this.client.send('503 5.5.1 Error: send HELO/EHLO first'); } if (this.server.options.requireAuthentication && !this.authentication.authenticated) { return this.client.send('530 5.5.1 Authentication Required'); } if (this.envelope.from) { return this.client.send('503 5.5.1 Error: nested MAIL command'); } if (!(match = mail.match(/^from\:\s*<([^@>]+\@([^@>]+))>(\s|$)/i)) && !(mail.match(/^from\:\s*<>/i))) { return this.client.send('501 5.1.7 Bad sender address syntax'); } if (this.server.options.maxSize) { mail.replace(/> size=(\d+)\b\s*/i, function(o, size) { self.envelope.messageSize = size; }); } email = (match !== null && match[1]) || ''; domain = ((match !== null && match[2]) || '').toLowerCase(); this._validateAddress('sender', email, domain, function(err) { if (err) { return self.client.send(err.message); } email = email.substr(0, email.length - domain.length) + domain; self.envelope.from = email; self.client.send('250 2.1.0 Ok'); }); }; /** * <p>Add recipients to the e-mail envelope</p> * * <p>This doesn't work if <code>MAIL</code> command is not yet executed</p> * * <p>If <code>validateRecipients</code> option is set to true, then emits * <code>'validateRecipient'</code> and wait for the callback before moving * on</p> * * @param {String} mail Address payload in the form of 'TO:<address>' */ SMTPServerConnection.prototype._onCommandRCPT = function(mail) { var self = this, match, email, domain; if (!this.envelope.from) { return this.client.send('503 5.5.1 Error: need MAIL command'); } if (!(match = mail.match(/^to\:\s*<([^@>]+\@([^@>]+))>$/i))) { return this.client.send('501 5.1.7 Bad recipient address syntax'); } email = match[1] || ''; domain = (match[2] || '').toLowerCase(); this._validateAddress('recipient', email, domain, function(err) { if (err) { return self.client.send(err.message); } // force domain part to be lowercase email = email.substr(0, email.length - domain.length) + domain; // add to recipients list if (self.envelope.to.indexOf(email) < 0) { self.envelope.to.push(email); } self.client.send('250 2.1.0 Ok'); }); }; /** * <p>If <code>disableDNSValidation</code> option is set to false, then performs * validation via DNS lookup. * * <p>If <code>validate{type}</code> option is set to true, then emits * <code>'validate{type}'</code> and waits for the callback before moving * on</p> * * @param {String} addressType 'sender' or 'recipient' * @param {String} email * @param {String} domain * @param {Function} callback */ SMTPServerConnection.prototype._validateAddress = function(addressType, email, domain, callback) { var validateEvent, validationFailedEvent, dnsErrorMessage, localErrorMessage; if (addressType === 'sender') { validateEvent = 'validateSender'; validationFailedEvent = 'senderValidationFailed'; dnsErrorMessage = '450 4.1.8 <' + email + '>: Sender address rejected: Domain not found'; localErrorMessage = '550 5.1.1 <' + email + '>: Sender address rejected: User unknown in local sender table'; } else if (addressType === 'recipient') { validateEvent = 'validateRecipient'; validationFailedEvent = 'recipientValidationFailed'; dnsErrorMessage = '450 4.1.8 <' + email + '>: Recipient address rejected: Domain not found'; localErrorMessage = '550 5.1.1 <' + email + '>: Recipient address rejected: User unknown in local recipient table'; } else { // How are internal errors handled? throw new Error('Address type not supported'); } var validateViaLocal = function() { if (this.server.listeners(validateEvent).length) { this.server.emit(validateEvent, this.envelope, email, (function(err) { if (err) { return callback(new Error(err.SMTPResponse || localErrorMessage)); } return callback(); }).bind(this)); } else { return callback(); } }; var validateViaDNS = function() { dnslib.resolveMx(domain, (function(err, addresses) { if (err || !addresses || !addresses.length) { this.server.emit(validationFailedEvent, email); return callback(new Error(err && err.SMTPResponse || dnsErrorMessage)); } validateViaLocal.call(this); }).bind(this)); }; if (!this.server.options.disableDNSValidation) { validateViaDNS.call(this); } else { return validateViaLocal.call(this); } }; /** * <p>Switch to data mode and starts waiting for a binary data stream. Emits * <code>'startData'</code>.</p> * * <p>If <code>RCPT</code> is not yet run, stop</p> */ SMTPServerConnection.prototype._onCommandDATA = function() { if (!this.envelope.to.length) { return this.client.send('503 5.5.1 Error: need RCPT command'); } this.client.startDataMode(); this.client.send('354 End data with <CR><LF>.<CR><LF>'); this.server.emit('startData', this.envelope); }; /** * <p>Resets the current state - e-mail data and authentication info</p> */ SMTPServerConnection.prototype._onCommandRSET = function() { this.init(); this.client.send('250 2.0.0 Ok'); }; /** * <p>If the server is in secure connection mode, start the authentication * process. Param <code>payload</code> defines the authentication mechanism.</p> * * <p>Currently supported - PLAIN and LOGIN. There is no need for more * complicated mechanisms (different CRAM versions etc.) since authentication * is only done in secure connection mode</p> * * @param {Buffer} payload Defines the authentication mechanism */ SMTPServerConnection.prototype._onCommandAUTH = function(payload) { var method; if (!this.server.options.requireAuthentication && !this.server.options.enableAuthentication) { return this.client.send('503 5.5.1 Error: authentication not enabled'); } if (!this.server.options.ignoreTLS && !this.client.secureConnection) { return this.client.send('530 5.7.0 Must issue a STARTTLS command first'); } if (this.authentication.authenticated) { return this.client.send('503 5.7.0 No identity changes permitted'); } payload = payload.toString('utf-8').trim().split(' '); method = payload.shift().trim().toUpperCase(); if (this.server.options.authMethods.indexOf(method) < 0) { return this.client.send('535 5.7.8 Error: authentication failed: no mechanism available'); } switch (method) { case 'PLAIN': this._handleAuthPlain(payload); break; case 'XOAUTH2': this._handleAuthXOAuth2(payload); break; case 'LOGIN': var username = payload.shift(); if (username) { username = username.trim(); this.authentication.state = 'AUTHENTICATING'; } this._handleAuthLogin(username); break; } }; /** * <p>Upgrade the connection to a secure TLS connection</p> */ SMTPServerConnection.prototype._onCommandSTARTTLS = function() { if(this.server.options.disableSTARTTLS) { return this.client.send('502 5.5.2 Error: command not recognized'); } if (this.client.secureConnection) { return this.client.send('554 5.5.1 Error: TLS already active'); } this.client.send('220 2.0.0 Ready to start TLS'); this.client.startTLS(this.server.options.credentials, (function() { // Connection secured // nothing to do here, since it is the client that should // make the next move }).bind(this)); }; /** * <p>Retrieve hostname from the client. Not very important, since client * IP is already known and the client can send fake data</p> * * @param {String} host Hostname of the client */ SMTPServerConnection.prototype._onCommandHELO = function(host) { if (!host) { return this.client.send('501 Syntax: EHLO hostname'); } else { this.hostNameAppearsAs = host; this.envelope.host = host; } this.client.send('250 ' + this.server.options.name + ' at your service, [' + this.client.remoteAddress + ']'); }; /** * <p>Retrieve hostname from the client. Not very important, since client * IP is already known and the client can send fake data</p> * * <p>Additionally displays server capability list to the client</p> * * @param {String} host Hostname of the client */ SMTPServerConnection.prototype._onCommandEHLO = function(host) { var response = [this.server.options.name + ' at your service, [' + this.client.remoteAddress + ']', '8BITMIME', 'ENHANCEDSTATUSCODES' ]; if (this.server.options.maxSize) { response.push('SIZE ' + this.server.options.maxSize); } if ((this.client.secureConnection || this.server.options.ignoreTLS) && (this.server.options.requireAuthentication || this.server.options.enableAuthentication)) { response.push('AUTH ' + this.server.options.authMethods.join(' ')); response.push('AUTH=' + this.server.options.authMethods.join(' ')); } if (!this.client.secureConnection && !this.server.options.disableSTARTTLS) { response.push('STARTTLS'); } if (!host) { return this.client.send('501 Syntax: EHLO hostname'); } else { this.hostNameAppearsAs = host; this.envelope.host = host; } this.client.send(response.map(function(feature, i, arr) { return '250' + (i < arr.length - 1 ? '-' : ' ') + feature; }).join('\r\n')); }; /** * <p>No operation. Just returns OK.</p> */ SMTPServerConnection.prototype._onCommandNOOP = function() { this.client.send('250 OK'); }; /** * <p>Detect login information from the payload and initiate authentication * by emitting <code>'authorizeUser'</code> and waiting for its callback</p> * * @param {Buffer} payload AUTH PLAIN login information */ SMTPServerConnection.prototype._handleAuthPlain = function(payload) { if (payload.length) { var userdata = new Buffer(payload.join(' '), 'base64'), password; userdata = userdata.toString('utf-8').split('\u0000'); if (userdata.length != 3) { return this.client.send('500 5.5.2 Error: invalid userdata to decode'); } this.authentication.username = userdata[1] || userdata[0] || ''; password = userdata[2] || ''; this.server.emit('authorizeUser', this.envelope, this.authentication.username, password, (function(err, success) { if (err || !success) { this.authentication.authenticated = false; this.authentication.username = false; this.authentication.state = 'NORMAL'; return this.client.send('535 5.7.8 Error: authentication failed: generic failure'); } this.client.send('235 2.7.0 Authentication successful'); this.authentication.authenticated = true; this.authentication.state = 'AUTHENTICATED'; }).bind(this)); } else { if (this.authentication.state == 'NORMAL') { this.authentication.state = 'AUTHPLAINUSERDATA'; this.client.send('334'); } } }; /** * <p>Sets authorization state to 'AUTHENTICATING' and reuqests for the * username and password from the client</p> * * <p>If username and password are set initiate authentication * by emitting <code>'authorizeUser'</code> and waiting for its callback</p> * * @param {Buffer} payload AUTH LOGIN login information */ SMTPServerConnection.prototype._handleAuthLogin = function(payload) { if (this.authentication.state == 'NORMAL') { this.authentication.state = 'AUTHENTICATING'; this.client.send('334 VXNlcm5hbWU6'); } else if (this.authentication.state == 'AUTHENTICATING') { if (this.authentication.username === false) { this.authentication.username = new Buffer(payload, 'base64').toString('utf-8'); this.client.send('334 UGFzc3dvcmQ6'); } else { this.authentication.state = 'VERIFYING'; this.server.emit('authorizeUser', this.envelope, this.authentication.username, new Buffer(payload, 'base64').toString('utf-8'), (function(err, success) { if (err || !success) { this.authentication.authenticated = false; this.authentication.username = false; this.authentication.state = 'NORMAL'; return this.client.send('535 5.7.8 Error: authentication failed: generic failure'); } this.client.send('235 2.7.0 Authentication successful'); this.authentication.authenticated = true; this.authentication.state = 'AUTHENTICATED'; }).bind(this)); } } }; /** * <p>Detect login information from the payload and initiate authentication * by emitting <code>'authorizeUser'</code> and waiting for its callback</p> * * @param {Buffer} payload AUTH XOAUTH2 login information */ SMTPServerConnection.prototype._handleAuthXOAuth2 = function(payload) { if (this.authentication.state == 'AUTHXOAUTH2') { // empty response from the client this.authentication.authenticated = false; this.authentication.username = false; this.authentication.state = 'NORMAL'; return this.client.send('535 5.7.1 Username and Password not accepted'); } var userdata = new Buffer(payload.join(' '), 'base64'), token; userdata = userdata.toString('utf-8').split('\x01'); if (userdata.length != 4) { return this.client.send('500 5.5.2 Error: invalid userdata to decode'); } this.authentication.username = userdata[0].substr(5) || ''; token = userdata[1].split(' ')[1] || ''; this.server.emit('authorizeUser', this.envelope, this.authentication.username, token, (function(err, success) { if (err || !success) { this.authentication.state = 'AUTHXOAUTH2'; return this.client.send('334 eyJzdGF0dXMiOiI0MDEiLCJzY2hlbWVzIjoiYmVhcmVyIG1hYyIsInNjb3BlIjoiaHR0cHM6Ly9tYWlsLmdvb2dsZS5jb20vIn0K'); } this.client.send('235 2.7.0 Authentication successful'); this.authentication.authenticated = true; this.authentication.state = 'AUTHENTICATED'; }).bind(this)); }; /** * <p>Emits the data received from the client with <code>'data'</code> * * @event * @param {Buffer} chunk Binary data sent by the client on data mode */ SMTPServerConnection.prototype._onData = function(chunk) { this.server.emit('data', this.envelope, chunk); }; /** * <p>If the data stream ends, emit <code>'dataReady'</code>and wait for * the callback, only if server listened for it.</p> * * @event */ SMTPServerConnection.prototype._onDataReady = function() { if (this.server.listeners('dataReady').length) { this.server.emit('dataReady', this.envelope, (function(err, code) { this.init(true); //reset state, keep auth data if (err) { this.client.send(err && err.SMTPResponse || ('550 ' + (err && err.message || 'FAILED'))); } else { this.client.send('250 2.0.0 Ok: queued as ' + (code || crypto.randomBytes(10).toString('hex'))); } }).bind(this)); } else { this.init(true); //reset state, keep auth data this.client.send('250 2.0.0 Ok: queued as ' + crypto.randomBytes(10).toString('hex')); } };