pax_global_header 0000666 0000000 0000000 00000000064 12470461415 0014516 g ustar 00root root 0000000 0000000 52 comment=2ac1b730f56b8a19f767268430bddb85a217784c simplesmtp-0.3.35/ 0000775 0000000 0000000 00000000000 12470461415 0014003 5 ustar 00root root 0000000 0000000 simplesmtp-0.3.35/.gitignore 0000664 0000000 0000000 00000000027 12470461415 0015772 0 ustar 00root root 0000000 0000000 node_modules .DS_Store simplesmtp-0.3.35/.npmignore 0000664 0000000 0000000 00000000032 12470461415 0015775 0 ustar 00root root 0000000 0000000 .travis.yml test examples simplesmtp-0.3.35/.travis.yml 0000664 0000000 0000000 00000000236 12470461415 0016115 0 ustar 00root root 0000000 0000000 language: node_js node_js: - 0.8 - "0.10" notifications: email: recipients: - andris@kreata.ee on_success: change on_failure: change simplesmtp-0.3.35/CHANGELOG.md 0000664 0000000 0000000 00000011513 12470461415 0015615 0 ustar 00root root 0000000 0000000 # CHANGELOG ## v0.3.34 2014-01-14 * Bumped version to 0.3.34 * Fixed a bug with ES6 strict mode (can't set properties to a `false` value) ## v0.3.33 2014-09-04 * Bumped version to 0.3.33 * Added deprecation notice ## v0.3.32 2014-05-30 * Bumped version to 0.3.32 * ignore close if end was already called [004ebaee] ## v0.3.30 2014-05-13 * Bumped version to 0.3.30 * Added .npmignore [a7344b49] ## v0.3.29 2014-05-07 * Bumped version to 0.3.29 * Changed formatting rules, use single quotes instead of double quotes [92b581c8] * rollback NOOP usage [e47e24bb] ## v0.3.28 2014-05-03 * Bumped version to 0.3.28 * handle errors with NOOP [deb18352] ## v0.3.27 2014-04-23 * Bumped version to 0.3.27 * get tests running in node 0.8, 0.10, 0.11 [9b3f9043..833388d5] ## v0.3.26 2014-04-23 * Bumped version to 0.3.26 * Server: Added support for XOAUTH2 authentication [87b6ed66] * Client: Use interval NOOPing to keep the connection up [184d8623] * Client: do not throw if recipients are note set [785a2b09] ## v0.3.25 2014-04-16 * Bumped version to 0.3.25 * disabled server test for max incoming connections [476f8cf5] * Added socketTimeout option [b83a4838] * fix invalid tests [cf22d390] ## v0.3.24 2014-03-31 * Bumped version to 0.3.24 * Added test for empty MAIL FROM [7f17174d] * Allow null return sender in mail command (coxeh) [08bc6a6f] * incorrect mail format fix (siterra) [d42d364e] * support for `form` and `to` structure: {address:"...",name:"..."} siterra) [2b054740] * Improved auth supports detection (finian) [863dc019] * Fixed a Buffer building bug (finian) [6dc9a4e2] ## v0.3.23 2014-03-10 * Bumped version to 0.3.23 * removed pipelining [4f0a382f] * Rename disableDotEscaping to enableDotEscaping [5534bd85] * Ignore OAuth2 errors from destroyed connections (SLaks) [e8ff3356] ## v0.3.22 2014-02-16 * Bumped version to 0.3.22 * Emit error on unexpected close [111da167] * Allowed persistence of custom properties when resetting envelope state. (garbetjie) [b49b7ead] ## v0.3.21 2014-02-16 * Bumped version to 0.3.21 * Ignore OAuth errors from destroyed connections (SLaks) [d50a7571] ## v0.3.20 2014-01-28 * Bumped version to 0.3.20 * Re-emit 'drain' from tcp socket [5bfb1fcc] ## v0.3.19 2014-01-28 * Bumped version to 0.3.19 * Prefer setImmediate over nextTick if available [f53e2d44] * Server: Implemented "NOOP" command (codingphil) [707485c0] * Server: Allow SIZE with MAIL [3b404028] ## v0.3.18 2014-01-05 * Bumped version to 0.3.18 * Added limiting of max client connections (garbetjie) [bcd5c0b3] ## v0.3.17 2014-01-05 * Bumped version to 0.3.17 * Do not create a server instance with invalid socket (47d17420) * typo (chrisdew) [fe4df83f] * Only emit rcptFailed if there actually was an address that was rejected [4c75523f] ## v0.3.16 2013-12-02 * Bumped version to 0.3.16 * Expose simplesmtp version number [c2382203] * typo in SMTP (chrisdew) [6c39a8d7] * Fix typo in README.md (Meekohi) [597a25cb] ## v0.3.15 2013-11-15 * Bumped version to 0.3.15 * Fixed bugs in connection timeout implementation (finian) [1a25d5af] ## v0.3.14 2013-11-08 * Bumped version to 0.3.14 * fixed: typo causing connection.remoteAddress to be undefined (johnnyleung) 795fe81f * improvements to handling stage (mysz) 5a79e6a1 * fixes TypeError: Cannot use 'in' operator to search for 'dsn' in undefined (mysz) 388d9b82 * lost saving stage in "DATA" (mysz) de694f67 * more info on smtp error (mysz) 42a4f964 ## v0.3.13 2013-10-29 * Bumped version to 0.3.13 * Handling errors which close connection on or before EHLO (mysz) 03345d4d ## v0.3.12 2013-10-29 * Bumped version to 0.3.12 * Allow setting maxMessages to pool 5d185708 ## v0.3.11 2013-10-22 * Bumped version to 0.3.11 * style update 2095d3a9 * fix tests 17a3632f * DSN Support implemented. (irvinzz) d1e8ba29 ## v0.3.10 2013-09-09 * Bumped version to 0.3.10 * added greetingTimeout, connectionTimeout and rejectUnathorized options to connection pool 8fa55cd3 ## v0.3.9 2013-09-09 * Bumped version to 0.3.9 * added "use strict" definitions, added new options for client: greetingTimeout, connectionTimeout, rejectUnathorized 51047ae0 * Do not include localAddress in the options if it is unset 7eb0e8fc ## v0.3.8 2013-08-21 * Bumped version to 0.3.8 * short fix for #42, Client parser hangs on certain input (dannycoates) 089f5cd4 ## v0.3.7 2013-08-16 * Bumped version to 0.3.7 * minor adjustments for better portability with browserify (whiteout-io) 15715498 * Added raw message to error object (andremetzen) 15715498 * Passing to error handler the message sent from SMTP server when an error occurred (andremetzen) 15d4cbb4 ## v0.3.6 2013-08-06 * Bumped version to 0.3.6 * Added changelog * Timeout if greeting is not received after connection is established simplesmtp-0.3.35/LICENSE 0000664 0000000 0000000 00000001646 12470461415 0015017 0 ustar 00root root 0000000 0000000 Copyright (c) 2012-2014 Andris Reinman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. simplesmtp-0.3.35/README.md 0000664 0000000 0000000 00000035053 12470461415 0015270 0 ustar 00root root 0000000 0000000 # simplesmtp ## DEPRECATION NOTICE This module is deprecated. For SMTP servers use [smtp-server](https://github.com/andris9/smtp-server), for SMTP clients use [smtp-connection](https://www.npmjs.org/package/smtp-connection). Alternatively, for full featured SMTP server applications, you should use [Haraka](https://www.npmjs.org/package/Haraka). -------- Simplesmtp is a module written for Node v0.6 and slightly updated for Node v0.8. It does not use Node v0.10 streams and probably is going to have a rocky future with Node v0.12. I do not have time to keep it up to date, the thing probably needs a major rewrite for Node v0.12. Should be fine though for integration testing purposes. ## Info This is a module to easily create custom SMTP servers and clients - use SMTP as a first class protocol in Node.JS! [](http://travis-ci.org/andris9/simplesmtp) [](http://badge.fury.io/js/simplesmtp) ## Version warning! If you are using node v0.6, then the last usable version of **simplesmtp** is v0.2.7 Current version of simplesmtp is fully supported for Node v0.8+ ˇ## SMTP Server ## Simple SMTP server For a simple inbound only, no authentication SMTP server you can use simplesmtp.createSimpleServer([options], requestListener).listen(port); Example simplesmtp.createSimpleServer({SMTPBanner:"My Server"}, function(req){ req.pipe(process.stdout); req.accept(); }).listen(port); Properties * **req.from** - From address * **req.to** - an array of To addresses * **req.host** - hostname reported by the client * **req.remodeAddress** - client IP address Methods * **req.accept** *([id])* - Accept the message with the selected ID * **req.reject** *([message])* - Reject the message with the selected message * **req.pipe** *(stream)* - Pipe the incoming data to a writable stream Events * **'data'** *(chunk)* - A chunk (Buffer) of the message. * **'end'** - The message has been transferred ## Advanced SMTP server ### Usage Create a new SMTP server instance with var smtp = simplesmtp.createServer([options]); And start listening on selected port smtp.listen(25, [function(err){}]); SMTP options can include the following: * **name** - the hostname of the server, will be used for informational messages * **debug** - if set to true, print out messages about the connection * **timeout** - client timeout in milliseconds, defaults to 60 000 (60 sec.) * **secureConnection** - start a server on secure connection * **SMTPBanner** - greeting banner that is sent to the client on connection * **requireAuthentication** - if set to true, require that the client must authenticate itself * **enableAuthentication** - if set to true, client may authenticate itself but don't have to (as opposed to `requireAuthentication` that explicitly requires clients to authenticate themselves) * **maxSize** - maximum size of an e-mail in bytes (currently informational only) * **credentials** - TLS credentials (`{key:'', cert:'', ca:['']}`) for the server * **authMethods** - allowed authentication methods, defaults to `["PLAIN", "LOGIN"]` * **disableEHLO** - if set to true, support HELO command only * **ignoreTLS** - if set to true, allow client do not use STARTTLS * **disableDNSValidation** - if set, do not validate sender domains * **disableSTARTTLS** - if set, do not use STARTTLS ### Example var simplesmtp = require("simplesmtp"), fs = require("fs"); var smtp = simplesmtp.createServer(); smtp.listen(25); smtp.on("startData", function(connection){ console.log("Message from:", connection.from); console.log("Message to:", connection.to); connection.saveStream = fs.createWriteStream("/tmp/message.txt"); }); smtp.on("data", function(connection, chunk){ connection.saveStream.write(chunk); }); smtp.on("dataReady", function(connection, callback){ connection.saveStream.end(); console.log("Incoming message saved to /tmp/message.txt"); callback(null, "ABC1"); // ABC1 is the queue id to be advertised to the client // callback(new Error("Rejected as spam!")); // reported back to the client }); ### Events * **startData** *(connection)* - DATA stream is opened by the client (`connection` is an object with `from`, `to`, `host` and `remoteAddress` properties) * **data** *(connection, chunk)* - e-mail data chunk is passed from the client * **dataReady** *(connection, callback)* - client is finished passing e-mail data, `callback` returns the queue id to the client * **authorizeUser** *(connection, username, password, callback)* - will be emitted if `requireAuthentication` option is set to true. `callback` has two parameters *(err, success)* where `success` is Boolean and should be true, if user is authenticated successfully * **validateSender** *(connection, email, callback)* - will be emitted if `validateSender` listener is set up * **validateRecipient** *(connection, email, callback)* - will be emitted it `validataRecipients` listener is set up * **close** *(connection)* - emitted when the connection to client is closed ## SMTP Client ### Usage SMTP client can be created with `simplesmtp.connect(port[,host][, options])` where * **port** is the port to connect to * **host** is the hostname to connect to (defaults to "localhost") * **options** is an optional options object (see below) ### Connection options The following connection options can be used with `simplesmtp.connect`: * **secureConnection** - use SSL * **name** - the name of the client server * **auth** - authentication object `{user:"...", pass:"..."}` or `{XOAuthToken:"base64data"}` * **ignoreTLS** - ignore server support for STARTTLS * **tls** - optional options object for `tls.connect`, also applies to STARTTLS. For example `rejectUnauthorized` is set to `false` by default. You can override this option by setting `tls: {rejectUnauthorized: true}` * **debug** - output client and server messages to console * **logFile** - optional filename where communication with remote server has to be logged * **instanceId** - unique instance id for debugging (will be output console with the messages) * **localAddress** - local interface to bind to for network connections (needs Node.js >= 0.11.3 for working with tls) * **greetingTimeout** (defaults to 10000) - Time to wait in ms until greeting message is received from the server * **connectionTimeout** (system default if not set) - Time to wait in ms until the socket is opened to the server * **socketTimeout** (defaults to 1 hour) - Time of inactivity until the connection is closed * **rejectUnathorized** (defaults to false) - if set to true accepts only valid server certificates. You can override this option with the `tls` option, this is just a shorthand * **dsn** - An object with methods `success`, `failure` and `delay`. If any of these are set to true, DSN will be used * **enableDotEscaping** set to true if you want to escape dots at the begining of each line. Defaults to false. ### Connection events Once a connection is set up the following events can be listened to: * **'idle'** - the connection to the SMTP server has been successfully set up and the client is waiting for an envelope * **'message'** - the envelope is passed successfully to the server and a message stream can be started * **'ready'** `(success)` - the message was sent * **'rcptFailed'** `(addresses)` - not all recipients were accepted (invalid addresses are included as an array) * **'error'** `(err, stage)` - An error occurred. The connection is closed and an 'end' event is emitted shortly. Second argument indicates on which SMTP session stage an error occured. * **'end'** - connection to the client is closed ### Sending an envelope When an `'idle'` event is emitted, an envelope object can be sent to the server. This includes a string `from` and an array of strings `to` property. Envelope can be sent with `client.useEnvelope(envelope)` // run only once as 'idle' is emitted again after message delivery client.once("idle", function(){ client.useEnvelope({ from: "me@example.com", to: ["receiver1@example.com", "receiver2@example.com"] }); }); The `to` part of the envelope includes **all** recipients from `To:`, `Cc:` and `Bcc:` fields. If setting the envelope up fails, an error is emitted. If only some (not all) recipients are not accepted, the mail can still be sent but an `rcptFailed` event is emitted. client.on("rcptFailed", function(addresses){ console.log("The following addresses were rejected: ", addresses); }); If the envelope is set up correctly a `'message'` event is emitted. ### Sending a message When `'message'` event is emitted, it is possible to send mail. To do this you can pipe directly a message source (for example an .eml file) to the client or alternatively you can send the message with `client.write` calls (you also need to call `client.end()` once the message is completed. If you are piping a stream to the client, do not leave the `'end'` event out, this is needed to complete the message sequence by the client. client.on("message", function(){ fs.createReadStream("test.eml").pipe(client); }); Once the message is delivered a `'ready'` event is emitted. The event has an parameter which indicates if the message was transmitted( (true) or not (false) and another which includes the last received data from the server. client.on("ready", function(success, response){ if(success){ console.log("The message was transmitted successfully with "+response); } }); ### XOAUTH **simplesmtp** supports [XOAUTH2 and XOAUTH](https://developers.google.com/google-apps/gmail/oauth_protocol) authentication. #### XOAUTH2 To use this feature you can set `XOAuth2` param as an `auth` option var mailOptions = { ..., auth:{ XOAuth2: { user: "example.user@gmail.com", clientId: "8819981768.apps.googleusercontent.com", clientSecret: "{client_secret}", refreshToken: "1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI", accessToken: "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==", timeout: 3600 } } } `accessToken` and `timeout` values are optional. If login fails a new access token is generated automatically. #### XOAUTH To use this feature you can set `XOAuthToken` param as an `auth` option var mailOptions = { ..., auth:{ XOAuthToken: "R0VUIGh0dHBzOi8vbWFpbC5nb29...." } } Alternatively it is also possible to use XOAuthToken generators (supported by Nodemailer) - this needs to be an object with a mandatory method `generate` that takes a callback function for generating a XOAUTH token string. This is better for generating tokens only when needed - there is no need to calculate unique token for every e-mail request, since a lot of these might share the same connection and thus the cleint needs not to re-authenticate itself with another token. var XOGen = { token: "abc", generate: function(callback){ if(1 != 1){ return callback(new Error("Tokens can't be generated in strange environments")); } callback(null, new Buffer(this.token, "utf-8").toString("base64")); } } var mailOptions = { ..., auth:{ XOAuthToken: XOGen } } ### Error types Emitted errors include the reason for failing in the `name` property * **UnknowAuthError** - the client tried to authenticate but the method was not supported * **AuthError** - the username/password used were rejected * **TLSError** - STARTTLS failed * **SenderError** - the sender e-mail address was rejected * **RecipientError** - all recipients were rejected (if only some of the recipients are rejected, a `'rcptFailed'` event is raised instead There's also an additional property in the error object called `data` that includes the last response received from the server (if available for the current error type). ### About reusing the connection You can reuse the same connection several times but you can't send a mail through the same connection concurrently. So if you catch and `'idle'` event lock the connection to a message process and unlock after `'ready'`. On `'error'` events you should reschedule the message and on `'end'` events you should recreate the connection. ### Closing the client By default the client tries to keep the connection up. If you want to close it, run `client.quit()` - this sends a `QUIT` command to the server and closes the connection client.quit(); ## SMTP Client Connection pool **simplesmtp** has the option for connection pooling if you want to reuse a bulk of connections. ### Usage Create a connection pool of SMTP clients with simplesmtp.createClientPool(port[,host][, options]) where * **port** is the port to connect to * **host** is the hostname to connect to (defaults to "localhost") * **options** is an optional options object (see below) ### Connection options The following connection options can be used with `simplesmtp.connect`: * **secureConnection** - use SSL * **name** - the name of the client server * **auth** - authentication object `{user:"...", pass:"..."}` or `{XOAuthToken:"base64data"}` * **ignoreTLS** - ignore server support for STARTTLS * **debug** - output client and server messages to console * **logFile** - optional filename where communication with remote server has to be logged * **maxConnections** - how many connections to keep in the pool (defaults to 5) * **localAddress** - local interface to bind to for network connections (needs Node.js >= 0.11.3 for working with tls) * **maxMessages** - limit the count of messages to send through a single connection (no limit by default) ### Send an e-mail E-mails can be sent through the pool with pool.sendMail(mail[, callback]) where * **mail** is a [MailComposer](https://github.com/andris9/mailcomposer) compatible object * **callback** `(error, responseObj)` - is the callback function to run after the message is delivered or an error occured. `responseObj` may include `failedRecipients` which is an array with e-mail addresses that were rejected and `message` which is the last response from the server. ### Errors In addition to SMTP client errors another error name is used * **DeliveryError** - used if the message was not accepted by the SMTP server ## License **MIT** simplesmtp-0.3.35/examples/ 0000775 0000000 0000000 00000000000 12470461415 0015621 5 ustar 00root root 0000000 0000000 simplesmtp-0.3.35/examples/send.js 0000664 0000000 0000000 00000002166 12470461415 0017115 0 ustar 00root root 0000000 0000000 var simplesmtp = require('../index'); mail('sender@example.com', 'receiver@example.com', 'subject: test\r\n\r\nhello world!'); /** * Send a raw email * * @param {String} from E-mail address of the sender * @param {String|Array} to E-mail address or a list of addresses of the receiver * @param {[type]} message Mime message */ function mail(from, to, message) { var client = simplesmtp.connect(465, 'smtp.gmail.com', { secureConnection: true, auth: { user: 'gmail.username@gmail.com', pass: 'gmail_pass' }, debug: true }); client.once('idle', function() { client.useEnvelope({ from: from, to: [].concat(to || []) }); }); client.on('message', function() { client.write(message.replace(/\r?\n/g, '\r\n').replace(/^\./gm, '..')); client.end(); }); client.on('ready', function(success) { client.quit(); }); client.on('error', function(err) { console.log('ERROR'); console.log(err); }); client.on('end', function() { console.log('DONE') }); } simplesmtp-0.3.35/examples/simpleserver.js 0000664 0000000 0000000 00000001104 12470461415 0020673 0 ustar 00root root 0000000 0000000 "use strict"; //console.log(process.stdout.writable); var simplesmtp = require("../index"); simplesmtp.createSimpleServer({SMTPBanner:"My Server", debug: true}, function(req){ process.stdout.write("\r\nNew Mail:\r\n"); req.on("data", function(chunk){ process.stdout.write(chunk); }); req.accept(); }).listen(25, function(err){ if(!err){ console.log("SMTP server listening on port 25"); }else{ console.log("Could not start server on port 25. Ports under 1000 require root privileges."); console.log(err.message); } }); simplesmtp-0.3.35/examples/size.js 0000664 0000000 0000000 00000004012 12470461415 0017126 0 ustar 00root root 0000000 0000000 "use strict"; var simplesmtp = require("../index"), fs = require("fs"); // Example for http://tools.ietf.org/search/rfc1870 var maxMessageSize = 10; var smtp = simplesmtp.createServer({ maxSize: maxMessageSize, // maxSize must be set in order to support SIZE disableDNSValidation: true, debug: true }); smtp.listen(25); // Set up sender validation function smtp.on("validateSender", function(connection, email, done){ console.log(1, connection.messageSize, maxMessageSize); // SIZE value can be found from connection.messageSize if(connection.messageSize > maxMessageSize){ var err = new Error("Max space reached"); err.SMTPResponse = "452 This server can only accept messages up to " + maxMessageSize + " bytes"; done(err); }else{ done(); } }); // Set up recipient validation function smtp.on("validateRecipient", function(connection, email, done){ // Allow only messages up to 100 bytes if(connection.messageSize > 100){ var err = new Error("Max space reached"); err.SMTPResponse = "552 Channel size limit exceeded: " + email; done(err); }else{ done(); } }); smtp.on("startData", function(connection){ connection.messageSize = 0; connection.saveStream = fs.createWriteStream("/tmp/message.txt"); }); smtp.on("data", function(connection, chunk){ connection.messageSize += chunk.length; connection.saveStream.write(chunk); }); smtp.on("dataReady", function(connection, done){ connection.saveStream.end(); // check if message if(connection.messageSize > maxMessageSize){ // mail was too big and therefore ignored var err = new Error("Max fileSize reached"); err.SMTPResponse = "552 message exceeds fixed maximum message size"; done(err); }else{ done(); console.log("Delivered message by " + connection.from + " to " + connection.to.join(", ") + ", sent from " + connection.host + " (" + connection.remoteAddress + ")"); } }); simplesmtp-0.3.35/examples/validate-recipient.js 0000664 0000000 0000000 00000001751 12470461415 0021734 0 ustar 00root root 0000000 0000000 "use strict"; var simplesmtp = require("simplesmtp"), fs = require("fs"); var allowedRecipientDomains = ["node.ee", "neti.ee"]; var smtp = simplesmtp.createServer(); smtp.listen(25); // Set up recipient validation function smtp.on("validateRecipient", function(connection, email, done){ var domain = ((email || "").split("@").pop() || "").toLowerCase().trim(); if(allowedRecipientDomains.indexOf(domain) < 0){ done(new Error("Invalid domain")); }else{ done(); } }); smtp.on("startData", function(connection){ connection.saveStream = fs.createWriteStream("/tmp/message.txt"); }); smtp.on("data", function(connection, chunk){ connection.saveStream.write(chunk); }); smtp.on("dataReady", function(connection, done){ connection.saveStream.end(); done(); console.log("Delivered message by " + connection.from + " to " + connection.to.join(", ") + ", sent from " + connection.host + " (" + connection.remoteAddress + ")"); }); simplesmtp-0.3.35/index.js 0000664 0000000 0000000 00000000553 12470461415 0015453 0 ustar 00root root 0000000 0000000 var packageData = require('./package.json'); // expose the API to the world module.exports.createServer = require('./lib/server.js'); module.exports.createSimpleServer = require('./lib/simpleserver.js'); module.exports.connect = require('./lib/client.js'); module.exports.createClientPool = require('./lib/pool.js'); module.exports.version = packageData.version; simplesmtp-0.3.35/lib/ 0000775 0000000 0000000 00000000000 12470461415 0014551 5 ustar 00root root 0000000 0000000 simplesmtp-0.3.35/lib/client.js 0000664 0000000 0000000 00000103327 12470461415 0016373 0 ustar 00root root 0000000 0000000 'use strict'; var Stream = require('stream').Stream, utillib = require('util'), net = require('net'), tls = require('tls'), oslib = require('os'), xoauth2 = require('xoauth2'), crypto = require('crypto'), fs = require('fs'); // expose to the world module.exports = function(port, host, options) { var connection = new SMTPClient(port, host, options); if (typeof setImmediate == 'function') { setImmediate(connection.connect.bind(connection)); } else { process.nextTick(connection.connect.bind(connection)); } return connection; }; /** *
Generates a SMTP connection object
* *Optional options object takes the following possible properties:
*{user:'...', pass:'...'}
* Initializes instance variables
*/ SMTPClient.prototype._init = function() { /** * Defines if the current connection is secure or not. If not, * STARTTLS can be used if available * @private */ this._secureMode = false; /** * Ignore incoming data on TLS negotiation * @private */ this._ignoreData = false; /** * Store incomplete messages coming from the server * @private */ this._remainder = ''; /** * If set to true, then this object is no longer active * @private */ this.destroyed = false; /** * The socket connecting to the server * @publick */ this.socket = false; /** * Lists supported auth mechanisms * @private */ this._supportedAuth = []; /** * Currently in data transfer state * @private */ this._dataMode = false; /** * Keep track if the client sends a leading \r\n in data mode * @private */ this._lastDataBytes = new Buffer(2); this._lastDataBytes[0] = 0x0D; this._lastDataBytes[1] = 0x0A; /** * Function to run if a data chunk comes from the server * @private */ this._currentAction = false; /** * Timeout variable for waiting the greeting * @private */ this._greetingTimeout = false; /** * Timeout variable for waiting the connection to start * @private */ this._connectionTimeout = false; if (this.options.ignoreTLS || this.options.secureConnection) { this._secureMode = true; } /** * XOAuth2 token generator if XOAUTH2 auth is used * @private */ this._xoauth2 = false; if (typeof this.options.auth.XOAuth2 == 'object' && typeof this.options.auth.XOAuth2.getToken == 'function') { this._xoauth2 = this.options.auth.XOAuth2; } else if (typeof this.options.auth.XOAuth2 == 'object') { if (!this.options.auth.XOAuth2.user && this.options.auth.user) { this.options.auth.XOAuth2.user = this.options.auth.user; } this._xoauth2 = xoauth2.createXOAuth2Generator(this.options.auth.XOAuth2); } }; /** *Creates a connection to a SMTP server and sets up connection * listener
*/ SMTPClient.prototype.connect = function() { var opts = {}; if (this.options.secureConnection) { if (this.options.tls) { Object.keys(this.options.tls).forEach((function(key) { opts[key] = this.options.tls[key]; }).bind(this)); } if (!('rejectUnauthorized' in opts)) { opts.rejectUnauthorized = !! this.options.rejectUnauthorized; } if (this.options.localAddress) { opts.localAddress = this.options.localAddress; } this.socket = tls.connect(this.port, this.host, opts, this._onConnect.bind(this)); } else { opts = { port: this.port, host: this.host }; if (this.options.localAddress) { opts.localAddress = this.options.localAddress; } this.socket = net.connect(opts, this._onConnect.bind(this)); } if (this.options.connectionTimeout) { this._connectionTimeout = setTimeout((function() { var error = new Error('Connection timeout'); error.code = 'ETIMEDOUT'; error.errno = 'ETIMEDOUT'; error.stage = this.stage; this.emit('error', error); this.close(); }).bind(this), this.options.connectionTimeout); } this.socket.on('drain', this._onDrain.bind(this)); this.socket.on('error', this._onError.bind(this)); }; /** *Upgrades the connection to TLS
* * @param {Function} callback Callback function to run when the connection * has been secured */ SMTPClient.prototype._upgradeConnection = function(callback) { this._ignoreData = true; this.socket.removeAllListeners('data'); this.socket.removeAllListeners('error'); var opts = { socket: this.socket, host: this.host, rejectUnauthorized: !! this.options.rejectUnauthorized }; Object.keys(this.options.tls || {}).forEach((function(key) { opts[key] = this.options.tls[key]; }).bind(this)); this.socket = tls.connect(opts, (function() { this._ignoreData = false; this._secureMode = true; this.socket.on('data', this._onData.bind(this)); return callback(null, true); }).bind(this)); this.socket.on('error', this._onError.bind(this)); }; /** *Connection listener that is run when the connection to * the server is opened
* * @event */ SMTPClient.prototype._onConnect = function() { this.stage = 'connect'; clearTimeout(this._connectionTimeout); if ('setKeepAlive' in this.socket) { this.socket.setKeepAlive(true); } if ('setNoDelay' in this.socket) { this.socket.setNoDelay(true); } this.socket.on('data', this._onData.bind(this)); this.socket.on('close', this._onClose.bind(this)); this.socket.on('end', this._onEnd.bind(this)); this.socket.setTimeout(this.options.socketTimeout || (3 * 3600 * 1000)); // 1 hours this.socket.on('timeout', this._onTimeout.bind(this)); this._greetingTimeout = setTimeout((function() { // if still waiting for greeting, give up if (this.socket && !this._destroyed && this._currentAction == this._actionGreeting) { var error = new Error('Greeting never received'); error.code = 'ETIMEDOUT'; error.errno = 'ETIMEDOUT'; error.stage = this.stage; this.emit('error', error); this.close(); } }).bind(this), this.options.greetingTimeout || 10000); this._currentAction = this._actionGreeting; }; /** *Destroys the client - removes listeners etc.
*/ SMTPClient.prototype._destroy = function() { if (this._destroyed) { return; } this._destroyed = true; this._ignoreData = true; this.emit('end'); this.removeAllListeners(); // keep the error handler around, just in case this.socket.on('error', this._onError.bind(this)); }; /** *'data' listener for data coming from the server
* * @event * @param {Buffer} chunk Data chunk coming from the server */ SMTPClient.prototype._onData = function(chunk) { var str; if (this._ignoreData || !chunk || !chunk.length) { return; } // Wait until end of line if (chunk.readUInt8(chunk.length - 1) != 0x0A) { this._remainder += chunk.toString(); return; } else { str = (this._remainder + chunk.toString()).trim(); this._remainder = ''; } // if this is a multi line reply, wait until the ending if (str.match(/(?:^|\n)\d{3}-.+$/)) { this._remainder = str + '\r\n'; return; } if (this.options.debug) { console.log('SERVER' + (this.options.instanceId ? ' ' + this.options.instanceId : '') + ':\n└──' + str.replace(/\r?\n/g, '\n ')); } if (this.options.logFile) { this.log('SERVER' + (this.options.instanceId ? ' ' + this.options.instanceId : '') + ':\n└──' + str.replace(/\r?\n/g, '\n ')); } if (typeof this._currentAction == 'function') { this._currentAction.call(this, str); } }; /** *'error' listener for the socket
* * @event * @param {Error} err Error object * @param {String} type Error name */ SMTPClient.prototype._onError = function(err, type, data) { if (type && type != 'Error') { err.name = type; } if (data) { err.data = data; } err.stage = this.stage; this.emit('error', err); this.close(); }; /** *'drain' listener for the socket
* * @event */ SMTPClient.prototype._onDrain = function() { this.emit('drain'); }; /** *'close' listener for the socket
* * @event */ SMTPClient.prototype._onClose = function() { if ([this._actionGreeting, this._actionIdle, this.close].indexOf(this._currentAction) < 0 && !this._destroyed) { return this._onError(new Error('Connection closed unexpectedly')); } this.stage = 'close'; this._destroy(); }; /** *'end' listener for the socket
* * @event */ SMTPClient.prototype._onEnd = function() { this.stage = 'end'; this._destroy(); }; /** *'timeout' listener for the socket
* * @event */ SMTPClient.prototype._onTimeout = function() { this.close(); }; /** *Passes data stream to socket if in data mode
* * @param {Buffer} chunk Chunk of data to be sent to the server */ SMTPClient.prototype.write = function(chunk) { // works only in data mode if (!this._dataMode || this._destroyed) { // this line should never be reached but if it does, then // say act like everything's normal. return true; } if (typeof chunk == 'string') { chunk = new Buffer(chunk, 'utf-8'); } if (!this.options.enableDotEscaping) { if (chunk.length >= 2) { this._lastDataBytes[0] = chunk[chunk.length - 2]; this._lastDataBytes[1] = chunk[chunk.length - 1]; } else if (chunk.length == 1) { this._lastDataBytes[0] = this._lastDataBytes[1]; this._lastDataBytes[1] = chunk[0]; } } else { chunk = this._escapeDot(chunk); } if (this.options.debug) { console.log('CLIENT (DATA)' + (this.options.instanceId ? ' ' + this.options.instanceId : '') + ':\n└──' + chunk.toString().trim().replace(/\n/g, '\n ')); } if (this.options.logFile) { this.log('CLIENT (DATA)' + (this.options.instanceId ? ' ' + this.options.instanceId : '') + ':\n└──' + chunk.toString().trim().replace(/\n/g, '\n ')); } // pass the chunk to the socket return this.socket.write(chunk); }; /** *Indicates that a data stream for the socket is ended. Works only * in data mode.
* * @param {Buffer} [chunk] Chunk of data to be sent to the server */ SMTPClient.prototype.end = function(chunk) { // works only in data mode if (!this._dataMode || this._destroyed) { // this line should never be reached but if it does, then // say act like everything's normal. return true; } if (chunk && chunk.length) { this.write(chunk); } // redirect output from the server to _actionStream this._currentAction = this._actionStream; // indicate that the stream has ended by sending a single dot on its own line // if the client already closed the data with \r\n no need to do it again if (this._lastDataBytes[0] == 0x0D && this._lastDataBytes[1] == 0x0A) { this.socket.write(new Buffer('.\r\n', 'utf-8')); } else if (this._lastDataBytes[1] == 0x0D) { this.socket.write(new Buffer('\n.\r\n')); } else { this.socket.write(new Buffer('\r\n.\r\n')); } this._lastDataBytes[0] = 0x0D; this._lastDataBytes[1] = 0x0A; // end data mode this._dataMode = false; }; /** *Send a command to the server, append \r\n
* * @param {String} str String to be sent to the server */ SMTPClient.prototype.sendCommand = function(str) { if (this._destroyed) { // Connection already closed, can't send any more data return; } if (this.socket.destroyed) { return this.close(); } if (this.options.debug) { console.log('CLIENT' + (this.options.instanceId ? ' ' + this.options.instanceId : '') + ':\n└──' + (str || '').toString().trim().replace(/\n/g, '\n ')); } if (this.options.logFile) { this.log('CLIENT' + (this.options.instanceId ? ' ' + this.options.instanceId : '') + ':\n└──' + (str || '').toString().trim().replace(/\n/g, '\n ')); } this.socket.write(new Buffer(str + '\r\n', 'utf-8')); }; /** *Sends QUIT
*/ SMTPClient.prototype.quit = function() { this._closing = true; this.sendCommand('QUIT'); this._currentAction = this.close; }; /** *Closes the connection to the server
*/ SMTPClient.prototype.close = function() { this._closing = true; if (this.options.debug) { console.log('Closing connection to the server'); } if (this.options.logFile) { this.log('Closing connection to the server'); } var closeMethod = 'end'; // Clear current job this._currentAction = this._actionIdle; if (this.stage === 'init') { // Clear connection timeout timer if other than timeout error occurred clearTimeout(this._connectionTimeout); // Close the socket immediately when connection timed out closeMethod = 'destroy'; } if (this.socket && this.socket.socket && this.socket.socket[closeMethod] && !this.socket.socket.destroyed) { this.socket.socket[closeMethod](); } if (this.socket && this.socket[closeMethod] && !this.socket.destroyed) { this.socket[closeMethod](); } this._destroy(); }; /** *Initiates a new message by submitting envelope data, starting with
* MAIL FROM:
command
{from:'...', to:['...']}
* or
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
*/
SMTPClient.prototype.useEnvelope = function(envelope) {
this._envelope = envelope || {};
this._envelope.from = this._envelope.from && this._envelope.from.address || this._envelope.from || ('anonymous@' + this.options.name);
this._envelope.to = [].concat(this._envelope.to || []).map(function(to) {
return to && to.address || to;
});
// clone the recipients array for latter manipulation
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
this._envelope.rcptFailed = [];
this._currentAction = this._actionMAIL;
this.sendCommand('MAIL FROM:<' + (this._envelope.from) + '>');
};
/**
* If needed starts the authentication, if not emits 'idle' to * indicate that this client is ready to take in an outgoing mail
*/ SMTPClient.prototype._authenticateUser = function() { this.stage = 'auth'; if (!this.options.auth) { // no need to authenticate, at least no data given this._enterIdle(); return; } var auth; if (this.options.auth.XOAuthToken && this._supportedAuth.indexOf('XOAUTH') >= 0) { auth = 'XOAUTH'; } else if (this._xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) { auth = 'XOAUTH2'; } else if (this.options.authMethod) { auth = this.options.authMethod.toUpperCase().trim(); } else { // use first supported auth = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); } switch (auth) { case 'XOAUTH': this._currentAction = this._actionAUTHComplete; if (typeof this.options.auth.XOAuthToken == 'object' && typeof this.options.auth.XOAuthToken.generate == 'function') { this.options.auth.XOAuthToken.generate((function(err, XOAuthToken) { if (this._destroyed) { // Nothing to do here anymore, connection already closed return; } if (err) { return this._onError(err, 'XOAuthTokenError'); } this.sendCommand('AUTH XOAUTH ' + XOAuthToken); }).bind(this)); } else { this.sendCommand('AUTH XOAUTH ' + this.options.auth.XOAuthToken.toString()); } return; case 'XOAUTH2': this._currentAction = this._actionAUTHComplete; this._xoauth2.getToken((function(err, token) { if (this._destroyed) { // Nothing to do here anymore, connection already closed return; } if (err) { this._onError(err, 'XOAUTH2Error'); return; } this.sendCommand('AUTH XOAUTH2 ' + token); }).bind(this)); return; case 'LOGIN': this._currentAction = this._actionAUTH_LOGIN_USER; this.sendCommand('AUTH LOGIN'); return; case 'PLAIN': this._currentAction = this._actionAUTHComplete; this.sendCommand('AUTH PLAIN ' + new Buffer( //this.options.auth.user+'\u0000'+ '\u0000' + // skip authorization identity as it causes problems with some servers this.options.auth.user + '\u0000' + this.options.auth.pass, 'utf-8').toString('base64')); return; case 'CRAM-MD5': this._currentAction = this._actionAUTH_CRAM_MD5; this.sendCommand('AUTH CRAM-MD5'); return; } this._onError(new Error('Unknown authentication method - ' + auth), 'UnknowAuthError'); }; /** ACTIONS **/ /** *Will be run after the connection is created and the server sends * a greeting. If the incoming message starts with 220 initiate * SMTP session by sending EHLO command
* * @param {String} str Message from the server */ SMTPClient.prototype._actionGreeting = function(str) { this.stage = 'greeting'; clearTimeout(this._greetingTimeout); if (str.substr(0, 3) != '220') { this._onError(new Error('Invalid greeting from server - ' + str), false, str); return; } this._currentAction = this._actionEHLO; this.sendCommand('EHLO ' + this.options.name); }; /** *Handles server response for EHLO command. If it yielded in * error, try HELO instead, otherwise initiate TLS negotiation * if STARTTLS is supported by the server or move into the * authentication phase.
* * @param {String} str Message from the server */ SMTPClient.prototype._actionEHLO = function(str) { this.stage = 'ehlo'; if (str.substr(0, 3) == '421') { this._onError(new Error('Server terminates connection - ' + str), false, str); return; } if (str.charAt(0) != '2') { // Try HELO instead this._currentAction = this._actionHELO; this.sendCommand('HELO ' + this.options.name); return; } // Detect if the server supports STARTTLS if (!this._secureMode && str.match(/[ \-]STARTTLS\r?$/mi)) { this.sendCommand('STARTTLS'); this._currentAction = this._actionSTARTTLS; return; } // Detect if the server supports PLAIN auth if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i)) { this._supportedAuth.push('PLAIN'); } // Detect if the server supports LOGIN auth if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i)) { this._supportedAuth.push('LOGIN'); } // Detect if the server supports CRAM-MD5 auth if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i)) { this._supportedAuth.push('CRAM-MD5'); } // Detect if the server supports XOAUTH auth if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH/i)) { this._supportedAuth.push('XOAUTH'); } // Detect if the server supports XOAUTH2 auth if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i)) { this._supportedAuth.push('XOAUTH2'); } this._authenticateUser.call(this); }; /** *Handles server response for HELO command. If it yielded in * error, emit 'error', otherwise move into the authentication phase.
* * @param {String} str Message from the server */ SMTPClient.prototype._actionHELO = function(str) { this.stage = 'helo'; if (str.charAt(0) != '2') { this._onError(new Error('Invalid response for EHLO/HELO - ' + str), false, str); return; } this._authenticateUser.call(this); }; /** *Handles server response for STARTTLS command. If there's an error * try HELO instead, otherwise initiate TLS upgrade. If the upgrade * succeedes restart the EHLO
* * @param {String} str Message from the server */ SMTPClient.prototype._actionSTARTTLS = function(str) { this.stage = 'starttls'; if (str.charAt(0) != '2') { // Try HELO instead this._currentAction = this._actionHELO; this.sendCommand('HELO ' + this.options.name); return; } this._upgradeConnection((function(err, secured) { if (err) { this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'TLSError'); return; } if (this.options.debug) { console.log('Connection secured'); } if (this.options.logFile) { this.log('Connection secured'); } if (secured) { // restart session this._currentAction = this._actionEHLO; this.sendCommand('EHLO ' + this.options.name); } else { this._authenticateUser.call(this); } }).bind(this)); }; /** *Handle the response for AUTH LOGIN command. We are expecting * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as * response needs to be base64 encoded username.
* * @param {String} str Message from the server */ SMTPClient.prototype._actionAUTH_LOGIN_USER = function(str) { if (str != '334 VXNlcm5hbWU6') { this._onError(new Error('Invalid login sequence while waiting for "334 VXNlcm5hbWU6" - ' + str), false, str); return; } this._currentAction = this._actionAUTH_LOGIN_PASS; this.sendCommand(new Buffer( this.options.auth.user + '', 'utf-8').toString('base64')); }; /** *Handle the response for AUTH CRAM-MD5 command. We are expecting
* '334
Handles the response to CRAM-MD5 authentication, if there's no error, * the user can be considered logged in. Emit 'idle' and start * waiting for a message to send
* * @param {String} str Message from the server */ SMTPClient.prototype._actionAUTH_CRAM_MD5_PASS = function(str) { if (!str.match(/^235\s+/)) { this._onError(new Error('Invalid login sequence while waiting for "235 go ahead" - ' + str), false, str); return; } this._enterIdle(); }; /** *Handle the response for AUTH LOGIN command. We are expecting * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as * response needs to be base64 encoded password.
* * @param {String} str Message from the server */ SMTPClient.prototype._actionAUTH_LOGIN_PASS = function(str) { if (str != '334 UGFzc3dvcmQ6') { this._onError(new Error('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6" - ' + str), false, str); return; } this._currentAction = this._actionAUTHComplete; this.sendCommand(new Buffer(this.options.auth.pass + '', 'utf-8').toString('base64')); }; /** *Handles the response for authentication, if there's no error, * the user can be considered logged in. Emit 'idle' and start * waiting for a message to send
* * @param {String} str Message from the server */ SMTPClient.prototype._actionAUTHComplete = function(str) { var response; if (this._xoauth2 && str.substr(0, 3) == '334') { try { response = str.split(' '); response.shift(); response = JSON.parse(new Buffer(response.join(' '), 'base64').toString('utf-8')); if ((!this._xoauth2.reconnectCount || this._xoauth2.reconnectCount < 200) && ['400', '401'].indexOf(response.status) >= 0) { this._xoauth2.reconnectCount = (this._xoauth2.reconnectCount || 0) + 1; this._currentAction = this._actionXOAUTHRetry; } else { this._xoauth2.reconnectCount = 0; this._currentAction = this._actionAUTHComplete; } this.sendCommand(new Buffer(0)); return; } catch (E) {} } if(this._xoauth2){ this._xoauth2.reconnectCount = 0; } if (str.charAt(0) != '2') { this._onError(new Error('Invalid login - ' + str), 'AuthError', str); return; } this._enterIdle(); }; /** * If XOAUTH2 authentication failed, try again by generating * new access token */ SMTPClient.prototype._actionXOAUTHRetry = function() { // ensure that something is listening unexpected responses this._currentAction = this._actionIdle; this._xoauth2.generateToken((function(err, token) { if (this._destroyed) { // Nothing to do here anymore, connection already closed return; } if (err) { this._onError(err, 'XOAUTH2Error'); return; } this._currentAction = this._actionAUTHComplete; this.sendCommand('AUTH XOAUTH2 ' + token); }).bind(this)); }; /** *This function is not expected to run. If it does then there's probably * an error (timeout etc.)
* * @param {String} str Message from the server */ SMTPClient.prototype._actionIdle = function(str) { this.stage = 'idle'; if (Number(str.charAt(0)) > 3) { this._onError(new Error(str), false, str); return; } // this line should never get called }; /** *Handle response for a MAIL FROM:
command
SetsUp DSN
*/ SMTPClient.prototype._getDSN = function() { var ret = '', n = [], dsn; if (this.currentMessage && this.currentMessage.options && 'dsn' in this.currentMessage.options) { dsn = this.currentMessage.options.dsn; if (dsn.success) { n.push('SUCCESS'); } if (dsn.failure) { n.push('FAILURE'); } if (dsn.delay) { n.push('DELAY'); } if (n.length > 0) { ret = ' NOTIFY=' + n.join(',') + ' ORCPT=rfc822;' + this.currentMessage._message.from; } } return ret; }; /** *Handle response for a RCPT TO:
command
Handle response for a DATA
command
Handle response for a DATA
stream
Log debugs to given file
* * @param {String} str Log message */ SMTPClient.prototype.log = function(str) { fs.appendFile(this.options.logFile, str + '\n', function(err) { if (err) { console.log('Log write failed. Data to log: ' + str); } }); }; /** *Inserts an extra dot at the begining of a line if it starts with a dot * See RFC 2821 Section 4.5.2
* * @param {Buffer} chunk The chunk that will be send. */ SMTPClient.prototype._escapeDot = function(chunk) { var pos, OutBuff, i; OutBuff = new Buffer(chunk.length * 2); pos = 0; for (i = 0; i < chunk.length; i++) { if (this._lastDataBytes[0] == 0x0D && this._lastDataBytes[1] == 0x0A && chunk[i] == 0x2E) { OutBuff[pos] = 0x2E; pos += 1; } OutBuff[pos] = chunk[i]; pos += 1; this._lastDataBytes[0] = this._lastDataBytes[1]; this._lastDataBytes[1] = chunk[i]; } return OutBuff.slice(0, pos); }; simplesmtp-0.3.35/lib/pool.js 0000664 0000000 0000000 00000027104 12470461415 0016064 0 ustar 00root root 0000000 0000000 'use strict'; var simplesmtp = require('../index'), EventEmitter = require('events').EventEmitter, utillib = require('util'), xoauth2 = require('xoauth2'); // expose to the world module.exports = function(port, host, options) { var pool = new SMTPConnectionPool(port, host, options); return pool; }; /** *Creates a SMTP connection pool
* *Optional options object takes the following possible properties:
*{user:'...', pass:'...'}
* Sends a message. If there's any idling connections available * use one to send the message immediatelly, otherwise add to queue.
* * @param {Object} message MailComposer object * @param {Function} callback Callback function to run on finish, gets an *error
object as a parameter if the sending failed
* and on success an object with failedRecipients
array as
* a list of addresses that were rejected (if any) and
* message
which indicates the last message received from
* the server
*/
SMTPConnectionPool.prototype.sendMail = function(message, callback) {
var connection;
message.returnCallback = callback;
if (this._connectionsAvailable.length) {
// if available connections pick one
connection = this._connectionsAvailable.pop();
this._connectionsInUse.push(connection);
this._processMessage(message, connection);
} else {
this._messageQueue.push(message);
if (this._connectionsAvailable.length + this._connectionsInUse.length < this.options.maxConnections) {
this._createConnection();
}
}
};
/**
* Closes all connections
*/ SMTPConnectionPool.prototype.close = function(callback) { var connection; // for some reason destroying the connections seem to be the only way :S while (this._connectionsAvailable.length) { connection = this._connectionsAvailable.pop(); connection.quit(); } while (this._connectionsInUse.length) { connection = this._connectionsInUse.pop(); connection.quit(); } if (callback) { if (typeof setImmediate == 'function') { setImmediate(callback); } else { process.nextTick(callback); } } }; /** *Initiates a connection to the SMTP server and adds it to the pool
*/ SMTPConnectionPool.prototype._createConnection = function() { var connectionOptions = { instanceId: ++this._idgen, debug: !! this.options.debug, logFile: this.options.logFile, ignoreTLS: !! this.options.ignoreTLS, tls: this.options.tls || false, auth: this.options.auth || false, authMethod: this.options.authMethod, name: this.options.name || false, secureConnection: !! this.options.secureConnection }, connection; if ('greetingTimeout' in this.options) { connectionOptions.greetingTimeout = this.options.greetingTimeout; } if ('socketTimeout' in this.options) { connectionOptions.socketTimeout = this.options.socketTimeout; } if ('connectionTimeout' in this.options) { connectionOptions.connectionTimeout = this.options.connectionTimeout; } if ('rejectUnathorized' in this.options) { connectionOptions.rejectUnathorized = this.options.rejectUnathorized; } if ('localAddress' in this.options) { connectionOptions.localAddress = this.options.localAddress; } connection = simplesmtp.connect(this.port, this.host, connectionOptions); connection._messagesProcessed = 0; connection.on('idle', this._onConnectionIdle.bind(this, connection)); connection.on('message', this._onConnectionMessage.bind(this, connection)); connection.on('ready', this._onConnectionReady.bind(this, connection)); connection.on('error', this._onConnectionError.bind(this, connection)); connection.on('end', this._onConnectionEnd.bind(this, connection)); connection.on('rcptFailed', this._onConnectionRCPTFailed.bind(this, connection)); this.emit('connectionCreated', connection); // as the connection is not ready yet, add to 'in use' queue this._connectionsInUse.push(connection); }; /** *Processes a message by assigning it to a connection object and initiating * the sending process by setting the envelope
* * @param {Object} message MailComposer message object * @param {Object} connectionsimplesmtp.connect
connection
*/
SMTPConnectionPool.prototype._processMessage = function(message, connection) {
connection.currentMessage = message;
message.currentConnection = connection;
connection._messagesProcessed++;
// send envelope
connection.useEnvelope(message.getEnvelope());
};
/**
* Will be fired on 'idle'
events by the connection, if
* there's a message currently in queue
Will be called when not all recipients were accepted
* * @event * @param {Object} connection Connection object that fired the event * @param {Array} addresses Failed addresses as an array of strings */ SMTPConnectionPool.prototype._onConnectionRCPTFailed = function(connection, addresses) { if (connection.currentMessage) { connection.currentMessage.failedRecipients = addresses; } }; /** *Will be called when the client is waiting for a message to deliver
* * @event * @param {Object} connection Connection object that fired the event */ SMTPConnectionPool.prototype._onConnectionMessage = function(connection) { if (connection.currentMessage) { connection.currentMessage.streamMessage(); connection.currentMessage.pipe(connection); } }; /** *Will be called when a message has been delivered
* * @event * @param {Object} connection Connection object that fired the event * @param {Boolean} success True if the message was queued by the SMTP server * @param {String} message Last message received from the server */ SMTPConnectionPool.prototype._onConnectionReady = function(connection, success, message) { var error, responseObj = {}; if (connection._messagesProcessed >= this.options.maxMessages && connection.socket) { connection.emit('end'); connection.removeAllListeners(); if (connection.socket) { connection.socket.destroy(); } this.emit('released', connection); } if (connection.currentMessage && connection.currentMessage.returnCallback) { if (success) { if (connection.currentMessage.failedRecipients) { responseObj.failedRecipients = connection.currentMessage.failedRecipients; } if (message) { responseObj.message = message; } if (connection.currentMessage._messageId) { responseObj.messageId = connection.currentMessage._messageId; } connection.currentMessage.returnCallback(null, responseObj); } else { error = new Error('Message delivery failed' + (message ? ': ' + message : '')); error.name = 'DeliveryError'; error.data = message; connection.currentMessage.returnCallback(error); } } connection.currentMessage = false; }; /** *Will be called when an error occurs
* * @event * @param {Object} connection Connection object that fired the event * @param {Object} error Error object */ SMTPConnectionPool.prototype._onConnectionError = function(connection, error) { var message = connection.currentMessage; connection.currentMessage = false; // clear a first message from the list, otherwise an infinite loop will emerge if (!message) { message = this._messageQueue.shift(); } if (message && message.returnCallback) { message.returnCallback(error); } }; /** *Will be called when a connection to the client is closed
* * @event * @param {Object} connection Connection object that fired the event */ SMTPConnectionPool.prototype._onConnectionEnd = function(connection) { var removed = false, i, len; // if in 'available' list, remove for (i = 0, len = this._connectionsAvailable.length; i < len; i++) { if (this._connectionsAvailable[i] == connection) { this._connectionsAvailable.splice(i, 1); // remove from list removed = true; break; } } if (!removed) { // if in 'in use' list, remove for (i = 0, len = this._connectionsInUse.length; i < len; i++) { if (this._connectionsInUse[i] == connection) { this._connectionsInUse.splice(i, 1); // remove from list removed = true; break; } } } // if there's still unprocessed mail and available connection slots, create // a new connection if (this._messageQueue.length && this._connectionsInUse.length + this._connectionsAvailable.length < this.options.maxConnections) { this._createConnection(); } }; simplesmtp-0.3.35/lib/server.js 0000664 0000000 0000000 00000064334 12470461415 0016427 0 ustar 00root root 0000000 0000000 'use strict'; /** * @fileOverview This is the main file for the simplesmtp library to create custom SMTP servers * @author Andris Reinman */ 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); }; /** *Constructs a SMTP server
* *Possible options are:
* *['PLAIN', 'LOGIN']
Closes the server
* * @param {Function} callback The callback function to run when the server is closed */ SMTPServer.prototype.end = function(callback) { this.SMTPServer.end(callback); }; /** *Creates a new {@link SMTPServerConnection} object and links the main server with * the client socket
* * @param {Object} client RAISocket object to a client */ SMTPServer.prototype._createSMTPServerConnection = function(client) { new SMTPServerConnection(this, client); }; /** *Sets up a handler for the connected client
* *Restarts the state and sets up event listeners for client actions
* * @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')); } } /** *Reset the envelope state
* *If keepAuthData
is set to true, then doesn't remove
* authentication data
Sends a message to the client and closes the connection
* * @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(); }; /** *Will be called when the connection to the client is closed
* * @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); }; /** *Will be called when timeout occurs
* * @event */ SMTPServerConnection.prototype._onTimeout = function() { this.end('421 4.4.2 ' + this.server.options.name + ' Error: timeout exceeded'); }; /** *Will be called when an error occurs
* * @event */ SMTPServerConnection.prototype._onError = function() { this.end('421 4.4.2 ' + this.server.options.name + ' Error: client error'); }; /** *Will be called when a command is received from the client
* *If there's curently an authentication process going on, route
* the data to _handleAuthLogin
, otherwise act as
* defined
Initiate an e-mail by defining a sender.
* *This doesn't work if authorization is required but the client is * not logged in yet.
* *If validateSender
option is set to true, then emits
* 'validateSender'
and wait for the callback before moving
* on
Add recipients to the e-mail envelope
* *This doesn't work if MAIL
command is not yet executed
If validateRecipients
option is set to true, then emits
* 'validateRecipient'
and wait for the callback before moving
* on
If disableDNSValidation
option is set to false, then performs
* validation via DNS lookup.
*
*
If validate{type}
option is set to true, then emits
* 'validate{type}'
and waits for the callback before moving
* on
Switch to data mode and starts waiting for a binary data stream. Emits
* 'startData'
.
If RCPT
is not yet run, stop
Resets the current state - e-mail data and authentication info
*/ SMTPServerConnection.prototype._onCommandRSET = function() { this.init(); this.client.send('250 2.0.0 Ok'); }; /** *If the server is in secure connection mode, start the authentication
* process. Param payload
defines the authentication mechanism.
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
* * @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; } }; /** *Upgrade the connection to a secure TLS connection
*/ 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)); }; /** *Retrieve hostname from the client. Not very important, since client * IP is already known and the client can send fake data
* * @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 + ']'); }; /** *Retrieve hostname from the client. Not very important, since client * IP is already known and the client can send fake data
* *Additionally displays server capability list to the client
* * @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')); }; /** *No operation. Just returns OK.
*/ SMTPServerConnection.prototype._onCommandNOOP = function() { this.client.send('250 OK'); }; /** *Detect login information from the payload and initiate authentication
* by emitting 'authorizeUser'
and waiting for its callback
Sets authorization state to 'AUTHENTICATING' and reuqests for the * username and password from the client
* *If username and password are set initiate authentication
* by emitting 'authorizeUser'
and waiting for its callback
Detect login information from the payload and initiate authentication
* by emitting 'authorizeUser'
and waiting for its callback
Emits the data received from the client with 'data'
*
* @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);
};
/**
*
If the data stream ends, emit 'dataReady'
and wait for
* the callback, only if server listened for it.