[Pkg-javascript-commits] [node-simplesmtp] 01/02: Imported Upstream version 0.3.35
Thorsten Alteholz
alteholz at moszumanska.debian.org
Sat Feb 27 14:07:56 UTC 2016
This is an automated email from the git hooks/post-receive script.
alteholz pushed a commit to branch master
in repository node-simplesmtp.
commit 26513ef326e9f788fb05695e7b031a3da424644f
Author: Thorsten Alteholz <debian at alteholz.de>
Date: Sat Feb 27 15:07:48 2016 +0100
Imported Upstream version 0.3.35
---
.gitignore | 2 +
.npmignore | 3 +
.travis.yml | 11 +
CHANGELOG.md | 170 ++++++
LICENSE | 16 +
README.md | 357 +++++++++++++
examples/send.js | 46 ++
examples/simpleserver.js | 19 +
examples/size.js | 67 +++
examples/validate-recipient.js | 37 ++
index.js | 8 +
lib/client.js | 1145 ++++++++++++++++++++++++++++++++++++++++
lib/pool.js | 374 +++++++++++++
lib/server.js | 804 ++++++++++++++++++++++++++++
lib/simpleserver.js | 126 +++++
package.json | 36 ++
test/client.js | 498 +++++++++++++++++
test/pool.js | 400 ++++++++++++++
test/server.js | 739 ++++++++++++++++++++++++++
test/testmessage.eml | 5 +
20 files changed, 4863 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd4f2b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.DS_Store
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..8a9ec42
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,3 @@
+.travis.yml
+test
+examples
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..7b2b382
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+language: node_js
+node_js:
+ - 0.8
+ - "0.10"
+
+notifications:
+ email:
+ recipients:
+ - andris at kreata.ee
+ on_success: change
+ on_failure: change
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..91718eb
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,170 @@
+# 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
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7d4a384
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,16 @@
+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.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..302a2bb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,357 @@
+# 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!
+
+[![Build Status](https://secure.travis-ci.org/andris9/simplesmtp.png)](http://travis-ci.org/andris9/simplesmtp)
+[![NPM version](https://badge.fury.io/js/simplesmtp.png)](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 at example.com",
+ to: ["receiver1 at example.com", "receiver2 at 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 at 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**
+
diff --git a/examples/send.js b/examples/send.js
new file mode 100644
index 0000000..6185a9c
--- /dev/null
+++ b/examples/send.js
@@ -0,0 +1,46 @@
+var simplesmtp = require('../index');
+
+mail('sender at example.com', 'receiver at 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 at 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')
+ });
+}
\ No newline at end of file
diff --git a/examples/simpleserver.js b/examples/simpleserver.js
new file mode 100644
index 0000000..2b44086
--- /dev/null
+++ b/examples/simpleserver.js
@@ -0,0 +1,19 @@
+"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);
+ }
+});
diff --git a/examples/size.js b/examples/size.js
new file mode 100644
index 0000000..4c24476
--- /dev/null
+++ b/examples/size.js
@@ -0,0 +1,67 @@
+"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 + ")");
+ }
+});
\ No newline at end of file
diff --git a/examples/validate-recipient.js b/examples/validate-recipient.js
new file mode 100644
index 0000000..a8c6237
--- /dev/null
+++ b/examples/validate-recipient.js
@@ -0,0 +1,37 @@
+"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 + ")");
+});
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..4f02973
--- /dev/null
+++ b/index.js
@@ -0,0 +1,8 @@
+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;
\ No newline at end of file
diff --git a/lib/client.js b/lib/client.js
new file mode 100644
index 0000000..d8adf31
--- /dev/null
+++ b/lib/client.js
@@ -0,0 +1,1145 @@
+'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;
+};
+
+/**
+ * <p>Generates a SMTP connection object</p>
+ *
+ * <p>Optional options object takes the following possible properties:</p>
+ * <ul>
+ * <li><b>secureConnection</b> - use SSL</li>
+ * <li><b>name</b> - the name of the client server</li>
+ * <li><b>auth</b> - authentication object <code>{user:'...', pass:'...'}</code>
+ * <li><b>ignoreTLS</b> - ignore server support for STARTTLS</li>
+ * <li><b>tls</b> - options for createCredentials</li>
+ * <li><b>debug</b> - output client and server messages to console</li>
+ * <li><b>logFile</b> - output client and server messages to file</li>
+ * <li><b>instanceId</b> - unique instance id for debugging</li>
+ * <li><b>localAddress</b> - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)</li>
+ * <li><b>greetingTimeout</b> - Time to wait in ms until greeting message is received from the server (defaults to 10000)</li>
+ * <li><b>socketTimeout</b> - Time of inactivity until the connection is closed (defaults to 1 hour)</li>
+ * </ul>
+ *
+ * @constructor
+ * @namespace SMTP Client module
+ * @param {Number} [port=25] Port number to connect to
+ * @param {String} [host='localhost'] Hostname to connect to
+ * @param {Object} [options] Option properties
+ */
+function SMTPClient(port, host, options) {
+ Stream.call(this);
+ this.writable = true;
+ this.readable = true;
+
+ this.stage = 'init';
+
+ this.options = options || {};
+
+ this.port = port || (this.options.secureConnection ? 465 : 25);
+ this.host = host || 'localhost';
+
+ this.options.secureConnection = !! this.options.secureConnection;
+ this.options.auth = this.options.auth || false;
+ this.options.maxConnections = this.options.maxConnections || 5;
+ this.options.enableDotEscaping = this.options.enableDotEscaping || false;
+
+ this._closing = false;
+
+ if (!this.options.name) {
+ // defaul hostname is machine hostname or [IP]
+ var defaultHostname = (oslib.hostname && oslib.hostname()) || '';
+
+ if (defaultHostname.indexOf('.') < 0) {
+ defaultHostname = '[127.0.0.1]';
+ }
+ if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
+ defaultHostname = '[' + defaultHostname + ']';
+ }
+
+ this.options.name = defaultHostname;
+ }
+
+ this._init();
+}
+utillib.inherits(SMTPClient, Stream);
+
+/**
+ * <p>Initializes instance variables</p>
+ */
+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);
+ }
+};
+
+/**
+ * <p>Creates a connection to a SMTP server and sets up connection
+ * listener</p>
+ */
+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));
+};
+
+/**
+ * <p>Upgrades the connection to TLS</p>
+ *
+ * @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));
+};
+
+/**
+ * <p>Connection listener that is run when the connection to
+ * the server is opened</p>
+ *
+ * @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;
+};
+
+/**
+ * <p>Destroys the client - removes listeners etc.</p>
+ */
+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));
+};
+
+/**
+ * <p>'data' listener for data coming from the server</p>
+ *
+ * @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);
+ }
+};
+
+/**
+ * <p>'error' listener for the socket</p>
+ *
+ * @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();
+};
+
+/**
+ * <p>'drain' listener for the socket</p>
+ *
+ * @event
+ */
+SMTPClient.prototype._onDrain = function() {
+ this.emit('drain');
+};
+
+
+/**
+ * <p>'close' listener for the socket</p>
+ *
+ * @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();
+};
+
+/**
+ * <p>'end' listener for the socket</p>
+ *
+ * @event
+ */
+SMTPClient.prototype._onEnd = function() {
+ this.stage = 'end';
+
+ this._destroy();
+};
+
+/**
+ * <p>'timeout' listener for the socket</p>
+ *
+ * @event
+ */
+SMTPClient.prototype._onTimeout = function() {
+ this.close();
+};
+
+/**
+ * <p>Passes data stream to socket if in data mode</p>
+ *
+ * @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);
+};
+
+/**
+ * <p>Indicates that a data stream for the socket is ended. Works only
+ * in data mode.</p>
+ *
+ * @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;
+};
+
+/**
+ * <p>Send a command to the server, append \r\n</p>
+ *
+ * @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'));
+};
+
+/**
+ * <p>Sends QUIT</p>
+ */
+SMTPClient.prototype.quit = function() {
+ this._closing = true;
+ this.sendCommand('QUIT');
+ this._currentAction = this.close;
+};
+
+/**
+ * <p>Closes the connection to the server</p>
+ */
+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();
+};
+
+/**
+ * <p>Initiates a new message by submitting envelope data, starting with
+ * <code>MAIL FROM:</code> command</p>
+ *
+ * @param {Object} envelope Envelope object in the form of
+ * <code>{from:'...', to:['...']}</code>
+ * or
+ * <code>{from:{address:'...',name:'...'}, to:[address:'...',name:'...']}</code>
+ */
+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) + '>');
+};
+
+/**
+ * <p>If needed starts the authentication, if not emits 'idle' to
+ * indicate that this client is ready to take in an outgoing mail</p>
+ */
+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 **/
+
+/**
+ * <p>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</p>
+ *
+ * @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);
+};
+
+/**
+ * <p>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.</p>
+ *
+ * @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);
+};
+
+/**
+ * <p>Handles server response for HELO command. If it yielded in
+ * error, emit 'error', otherwise move into the authentication phase.</p>
+ *
+ * @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);
+};
+
+/**
+ * <p>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</p>
+ *
+ * @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));
+};
+
+/**
+ * <p>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.</p>
+ *
+ * @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'));
+};
+
+/**
+ * <p>Handle the response for AUTH CRAM-MD5 command. We are expecting
+ * '334 <challenge string>'. Data to be sent as response needs to be
+ * base64 decoded challenge string, MD5 hashed using the password as
+ * a HMAC key, prefixed by the username and a space, and finally all
+ * base64 encoded again.</p>
+ *
+ * @param {String} str Message from the server
+ */
+SMTPClient.prototype._actionAUTH_CRAM_MD5 = function(str) {
+ var challengeMatch = str.match(/^334\s+(.+)$/),
+ challengeString = '';
+
+ if (!challengeMatch) {
+ this._onError(new Error('Invalid login sequence while waiting for server challenge string - ' + str), false, str);
+ return;
+ } else {
+ challengeString = challengeMatch[1];
+ }
+
+ // Decode from base64
+ var base64decoded = new Buffer(challengeString, 'base64').toString('ascii'),
+ hmac_md5 = crypto.createHmac('md5', this.options.auth.pass);
+ hmac_md5.update(base64decoded);
+ var hex_hmac = hmac_md5.digest('hex'),
+ prepended = this.options.auth.user + ' ' + hex_hmac;
+
+ this._currentAction = this._actionAUTH_CRAM_MD5_PASS;
+
+ this.sendCommand(new Buffer(prepended).toString('base64'));
+};
+
+/**
+ * <p>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</p>
+ *
+ * @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();
+};
+
+/**
+ * <p>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.</p>
+ *
+ * @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'));
+};
+
+/**
+ * <p>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</p>
+ *
+ * @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));
+};
+
+/**
+ * <p>This function is not expected to run. If it does then there's probably
+ * an error (timeout etc.)</p>
+ *
+ * @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
+};
+
+/**
+ * <p>Handle response for a <code>MAIL FROM:</code> command</p>
+ *
+ * @param {String} str Message from the server
+ */
+SMTPClient.prototype._actionMAIL = function(str) {
+ this.stage = 'mail';
+
+ if (Number(str.charAt(0)) != '2') {
+ this._onError(new Error('Mail from command failed - ' + str), 'SenderError', str);
+ return;
+ }
+
+ if (!this._envelope.rcptQueue.length) {
+ this._onError(new Error('Can\'t send mail - no recipients defined'), 'RecipientError', str);
+ } else {
+ this._envelope.curRecipient = this._envelope.rcptQueue.shift();
+ this._currentAction = this._actionRCPT;
+ this.sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>' + this._getDSN());
+ }
+};
+
+/**
+ * Emits 'idle'
+ */
+SMTPClient.prototype._enterIdle = function() {
+ this._currentAction = this._actionIdle;
+ this.emit('idle'); // ready to take orders
+};
+
+/**
+ * <p>SetsUp DSN</p>
+ */
+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;
+};
+
+/**
+ * <p>Handle response for a <code>RCPT TO:</code> command</p>
+ *
+ * @param {String} str Message from the server
+ */
+SMTPClient.prototype._actionRCPT = function(str) {
+ this.stage = 'rcpt';
+
+ if (Number(str.charAt(0)) != '2') {
+ // this is a soft error
+ this._envelope.rcptFailed.push(this._envelope.curRecipient);
+ }
+
+ if (!this._envelope.rcptQueue.length) {
+ if (this._envelope.rcptFailed.length < this._envelope.to.length) {
+ if (this._envelope.rcptFailed.length) {
+ this.emit('rcptFailed', this._envelope.rcptFailed);
+ }
+ this._currentAction = this._actionDATA;
+ this.sendCommand('DATA');
+ } else {
+ this._onError(new Error('Can\'t send mail - all recipients were rejected'), 'RecipientError', str);
+ return;
+ }
+ } else {
+ this._envelope.curRecipient = this._envelope.rcptQueue.shift();
+ this._currentAction = this._actionRCPT;
+ this.sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>');
+ }
+};
+
+/**
+ * <p>Handle response for a <code>DATA</code> command</p>
+ *
+ * @param {String} str Message from the server
+ */
+SMTPClient.prototype._actionDATA = function(str) {
+ this.stage = 'data';
+
+ // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
+ // some servers might use 250 instead, so lets check for 2 or 3 as the first digit
+ if ([2, 3].indexOf(Number(str.charAt(0))) < 0) {
+ this._onError(new Error('Data command failed - ' + str), false, str);
+ return;
+ }
+
+ // Emit that connection is set up for streaming
+ this._dataMode = true;
+ this._currentAction = this._actionIdle;
+ this.emit('message');
+};
+
+/**
+ * <p>Handle response for a <code>DATA</code> stream</p>
+ *
+ * @param {String} str Message from the server
+ */
+SMTPClient.prototype._actionStream = function(str) {
+ if (Number(str.charAt(0)) != '2') {
+ // Message failed
+ this.emit('ready', false, str);
+ } else {
+ // Message sent succesfully
+ this.emit('ready', true, str);
+ }
+
+ // Waiting for new connections
+ this._currentAction = this._actionIdle;
+
+ if (typeof setImmediate == 'function') {
+ setImmediate(this._enterIdle.bind(this));
+ } else {
+ process.nextTick(this._enterIdle.bind(this));
+ }
+};
+
+/**
+ * <p>Log debugs to given file</p>
+ *
+ * @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);
+ }
+ });
+};
+
+/**
+ * <p>Inserts an extra dot at the begining of a line if it starts with a dot
+ * See RFC 2821 Section 4.5.2</p>
+ *
+ * @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);
+};
\ No newline at end of file
diff --git a/lib/pool.js b/lib/pool.js
new file mode 100644
index 0000000..6ddd66e
--- /dev/null
+++ b/lib/pool.js
@@ -0,0 +1,374 @@
+'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;
+};
+
+/**
+ * <p>Creates a SMTP connection pool</p>
+ *
+ * <p>Optional options object takes the following possible properties:</p>
+ * <ul>
+ * <li><b>secureConnection</b> - use SSL</li>
+ * <li><b>name</b> - the name of the client server</li>
+ * <li><b>auth</b> - authentication object <code>{user:'...', pass:'...'}</code>
+ * <li><b>ignoreTLS</b> - ignore server support for STARTTLS</li>
+ * <li><b>tls</b> - options for createCredentials</li>
+ * <li><b>debug</b> - output client and server messages to console</li>
+ * <li><b>maxConnections</b> - how many connections to keep in the pool</li>
+ * </ul>
+ *
+ * @constructor
+ * @namespace SMTP Client Pool module
+ * @param {Number} [port=25] The port number to connecto to
+ * @param {String} [host='localhost'] THe hostname to connect to
+ * @param {Object} [options] optional options object
+ */
+function SMTPConnectionPool(port, host, options) {
+ EventEmitter.call(this);
+
+ /**
+ * Port number to connect to
+ * @public
+ */
+ this.port = port || 25;
+
+ /**
+ * Hostname to connect to
+ * @public
+ */
+ this.host = host || 'localhost';
+
+ /**
+ * Options object
+ * @public
+ */
+ this.options = options || {};
+ this.options.maxConnections = this.options.maxConnections || 5;
+ this.options.maxMessages = this.options.maxMessages || Infinity;
+
+ /**
+ * An array of connections that are currently idle
+ * @private
+ */
+ this._connectionsAvailable = [];
+
+ /**
+ * An array of connections that are currently in use
+ * @private
+ */
+ this._connectionsInUse = [];
+
+ /**
+ * Message queue (FIFO)
+ * @private
+ */
+ this._messageQueue = [];
+
+ /**
+ * Counter for generating ID values for debugging
+ * @private
+ */
+ this._idgen = 0;
+
+ // Initialize XOAUTH2 if needed
+ if (this.options.auth && 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.options.auth.XOAuth2 = xoauth2.createXOAuth2Generator(this.options.auth.XOAuth2);
+ }
+}
+utillib.inherits(SMTPConnectionPool, EventEmitter);
+
+/**
+ * <p>Sends a message. If there's any idling connections available
+ * use one to send the message immediatelly, otherwise add to queue.</p>
+ *
+ * @param {Object} message MailComposer object
+ * @param {Function} callback Callback function to run on finish, gets an
+ * <code>error</code> object as a parameter if the sending failed
+ * and on success an object with <code>failedRecipients</code> array as
+ * a list of addresses that were rejected (if any) and
+ * <code>message</code> 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();
+ }
+ }
+};
+
+/**
+ * <p>Closes all connections</p>
+ */
+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);
+ }
+ }
+};
+
+/**
+ * <p>Initiates a connection to the SMTP server and adds it to the pool</p>
+ */
+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);
+};
+
+/**
+ * <p>Processes a message by assigning it to a connection object and initiating
+ * the sending process by setting the envelope</p>
+ *
+ * @param {Object} message MailComposer message object
+ * @param {Object} connection <code>simplesmtp.connect</code> connection
+ */
+SMTPConnectionPool.prototype._processMessage = function(message, connection) {
+ connection.currentMessage = message;
+ message.currentConnection = connection;
+
+ connection._messagesProcessed++;
+
+ // send envelope
+ connection.useEnvelope(message.getEnvelope());
+};
+
+/**
+ * <p>Will be fired on <code>'idle'</code> events by the connection, if
+ * there's a message currently in queue</p>
+ *
+ * @event
+ * @param {Object} connection Connection object that fired the event
+ */
+SMTPConnectionPool.prototype._onConnectionIdle = function(connection) {
+ var message = this._messageQueue.shift();
+
+ if (message) {
+ this._processMessage(message, connection);
+ } else {
+ for (var i = 0, len = this._connectionsInUse.length; i < len; i++) {
+ if (this._connectionsInUse[i] == connection) {
+ this._connectionsInUse.splice(i, 1); // remove from list
+ break;
+ }
+ }
+ this._connectionsAvailable.push(connection);
+ }
+};
+
+/**
+ * <p>Will be called when not all recipients were accepted</p>
+ *
+ * @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;
+ }
+};
+
+/**
+ * <p>Will be called when the client is waiting for a message to deliver</p>
+ *
+ * @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);
+ }
+};
+
+/**
+ * <p>Will be called when a message has been delivered</p>
+ *
+ * @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;
+};
+
+/**
+ * <p>Will be called when an error occurs</p>
+ *
+ * @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);
+ }
+};
+
+/**
+ * <p>Will be called when a connection to the client is closed</p>
+ *
+ * @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();
+ }
+};
\ No newline at end of file
diff --git a/lib/server.js b/lib/server.js
new file mode 100644
index 0000000..5aab9fc
--- /dev/null
+++ b/lib/server.js
@@ -0,0 +1,804 @@
+'use strict';
+
+/**
+ * @fileOverview This is the main file for the simplesmtp library to create custom SMTP servers
+ * @author <a href='mailto:andris at 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'));
+ }
+};
\ No newline at end of file
diff --git a/lib/simpleserver.js b/lib/simpleserver.js
new file mode 100644
index 0000000..76ce239
--- /dev/null
+++ b/lib/simpleserver.js
@@ -0,0 +1,126 @@
+'use strict';
+
+var createSMTPServer = require('./server'),
+ Stream = require('stream').Stream,
+ utillib = require('util'),
+ oslib = require('os');
+
+module.exports = function(options, connectionCallback) {
+ return new SimpleServer(options, connectionCallback);
+};
+
+function SimpleServer(options, connectionCallback) {
+ if (!connectionCallback && typeof options == 'function') {
+ connectionCallback = options;
+ options = undefined;
+ }
+
+ this.connectionCallback = connectionCallback;
+
+ this.options = options || {};
+ this.initialChunk = true;
+
+ if (!('ignoreTLS' in this.options)) {
+ this.options.ignoreTLS = true;
+ }
+
+ if (!('disableDNSValidation' in this.options)) {
+ this.options.disableDNSValidation = true;
+ }
+
+ this.server = createSMTPServer(options);
+ this.listen = this.server.listen.bind(this.server);
+
+ this.server.on('startData', this._onStartData.bind(this));
+ this.server.on('data', this._onData.bind(this));
+ this.server.on('dataReady', this._onDataReady.bind(this));
+}
+
+SimpleServer.prototype._onStartData = function(connection) {
+ connection._session = new SimpleServerConnection(connection);
+ this.connectionCallback(connection._session);
+};
+
+SimpleServer.prototype._onData = function(connection, chunk) {
+ if (this.initialChunk) {
+ chunk = Buffer.concat([new Buffer(this._generateReceivedHeader(connection) + '\r\n', 'utf-8'), chunk]);
+ this.initialChunk = false;
+ }
+ connection._session.emit('data', chunk);
+};
+
+SimpleServer.prototype._onDataReady = function(connection, callback) {
+ connection._session._setCallback(callback);
+ connection._session.emit('end');
+};
+
+SimpleServer.prototype._generateReceivedHeader = function(connection) {
+ var parts = [];
+
+ if (connection.host && !connection.host.match(/^\[?\d+\.\d+\.\d+\.\d+\]?$/)) {
+ parts.push('from ' + connection.host);
+ parts.push('(' + connection.remoteAddress + ')');
+ } else {
+ parts.push('from ' + connection.remoteAddress);
+ }
+
+ parts.push('by ' + getHostName());
+
+ parts.push('with SMTP;');
+
+ parts.push(Date());
+
+ return 'Received: ' + parts.join(' ');
+};
+
+function SimpleServerConnection(connection) {
+ Stream.call(this);
+
+ this.accepted = false;
+ this.rejected = false;
+
+ this._callback = (function(err, code) {
+ if (err) {
+ this.rejected = err;
+ } else {
+ this.accepted = code || true;
+ }
+ });
+
+ ['from', 'to', 'host', 'remoteAddress'].forEach((function(key) {
+ if (connection[key]) {
+ this[key] = connection[key];
+ }
+ }).bind(this));
+}
+utillib.inherits(SimpleServerConnection, Stream);
+
+SimpleServerConnection.prototype._setCallback = function(callback) {
+
+ if (this.rejected) {
+ return callback(this.rejected);
+ } else if (this.accepted) {
+ return callback(null, this.accepted !== true ? this.accepted : undefined);
+ } else {
+ this._callback = callback;
+ }
+
+};
+
+SimpleServerConnection.prototype.pause = function() {};
+
+SimpleServerConnection.prototype.resume = function() {};
+
+SimpleServerConnection.prototype.accept = function(code) {
+ this._callback(null, code);
+};
+
+SimpleServerConnection.prototype.reject = function(reason) {
+ this._callback(new Error(reason || 'Rejected'));
+};
+
+function getHostName() {
+ return (oslib.hostname && oslib.hostname()) ||
+ (oslib.getHostname && oslib.getHostname()) ||
+ '127.0.0.1';
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e68d4c1
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "simplesmtp",
+ "description": "Simple SMTP server module to create custom SMTP servers",
+ "version": "0.3.35",
+ "author" : "Andris Reinman",
+ "maintainers":[
+ {
+ "name":"andris",
+ "email":"andris at node.ee"
+ }
+ ],
+ "repository" : {
+ "type" : "git",
+ "url" : "http://github.com/andris9/simplesmtp.git"
+ },
+ "scripts":{
+ "test": "nodeunit test/"
+ },
+ "main" : "./lib/smtp",
+ "licenses" : [
+ {
+ "type": "MIT",
+ "url": "http://github.com/andris9/simplesmtp/blob/master/LICENSE"
+ }
+ ],
+ "dependencies": {
+ "rai": "~0.1.11",
+ "xoauth2": "~0.1.8"
+ },
+ "devDependencies": {
+ "nodeunit": "*",
+ "mailcomposer": "*"
+ },
+ "engines" : { "node" : ">=0.8.0" },
+ "keywords": ["servers", "text-based", "smtp", "email", "mail", "e-mail"]
+}
diff --git a/test/client.js b/test/client.js
new file mode 100644
index 0000000..1cf9d1e
--- /dev/null
+++ b/test/client.js
@@ -0,0 +1,498 @@
+'use strict';
+
+var simplesmtp = require('../index'),
+ packageData = require('../package.json'),
+ fs = require('fs');
+
+process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+
+var PORT_NUMBER = 8397;
+
+exports['Version test'] = {
+ 'Should expose version number': function(test) {
+ test.ok(simplesmtp.version);
+ test.equal(simplesmtp.version, packageData.version);
+ test.done();
+ }
+};
+
+exports['General tests'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer();
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Connect and setup': function(test) {
+ var client = simplesmtp.connect(PORT_NUMBER);
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true);
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'socketTimeout': function(test) {
+ var client = simplesmtp.connect(PORT_NUMBER, false, {
+ socketTimeout: 500
+ });
+
+ var waitTimeout = setTimeout(function() {
+ test.ok(false);
+ test.done();
+ }, 2000);
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true);
+ });
+
+ client.on('error', function(err) {
+ test.ifError(err);
+ });
+
+ client.on('end', function() {
+ clearTimeout(waitTimeout);
+ test.done();
+ });
+ }
+};
+
+exports['Secure server'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer({
+ secureConnection: true
+ });
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Connect and setup': function(test) {
+ var client = simplesmtp.connect(PORT_NUMBER, false, {
+ secureConnection: true
+ });
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true);
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'Unsecure client should have timeout': function(test) {
+ var client = simplesmtp.connect(PORT_NUMBER, false, {
+ secureConnection: false
+ });
+
+ client.once('idle', function() {
+ test.ok(false);
+ });
+
+ client.on('error', function(err) {
+ test.equal(err.code, 'ETIMEDOUT');
+ client.close();
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ }
+};
+
+exports['Disabled EHLO'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer({
+ disableEHLO: true
+ });
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Connect and setup': function(test) {
+ var client = simplesmtp.connect(PORT_NUMBER, false, {});
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true);
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ }
+};
+
+exports['Authentication needed'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer({
+ requireAuthentication: true
+ });
+
+ this.server.on('authorizeUser', function(envelope, user, pass, callback) {
+ callback(null, user == 'test1' && pass == 'test2');
+ });
+
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Auth success': function(test) {
+ var client = simplesmtp.connect(PORT_NUMBER, false, {
+ auth: {
+ user: 'test1',
+ pass: 'test2'
+ }
+ });
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true);
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'Auth fails': function(test) {
+ var client = simplesmtp.connect(PORT_NUMBER, false, {
+ auth: {
+ user: 'test3',
+ pass: 'test4'
+ }
+ });
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(false); // should not occur
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(true); // login failed
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ }
+};
+
+exports['Message tests'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer({
+ validateSender: true,
+ validateRecipients: true
+ });
+
+ this.server.on('validateSender', function(envelope, email, callback) {
+ callback(email != 'test at pangalink.net' ? new Error('Failed sender') : null);
+ });
+
+ this.server.on('validateRecipient', function(envelope, email, callback) {
+ callback(email.split('@').pop() != 'pangalink.net' ? new Error('Failed recipient') : null);
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ callback(null, 'ABC1'); // ABC1 is the queue id to be advertised to the client
+ // callback(new Error('That was clearly a spam!'));
+ });
+
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Set envelope success': function(test) {
+ test.expect(2);
+
+ var client = simplesmtp.connect(PORT_NUMBER, false, {});
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for envelope
+
+ client.useEnvelope({
+ from: 'test at pangalink.net',
+ to: [
+ 'test1 at pangalink.net',
+ 'test2 at pangalink.net'
+ ]
+ });
+ });
+
+ client.on('message', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for message
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'Set envelope fails for sender': function(test) {
+ test.expect(2);
+
+ var client = simplesmtp.connect(PORT_NUMBER, false, {});
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for envelope
+
+ client.useEnvelope({
+ from: 'test3 at pangalink.net',
+ to: [
+ 'test1 at pangalink.net',
+ 'test2 at pangalink.net'
+ ]
+ });
+ });
+
+ client.on('message', function() {
+ // Client is ready to take messages
+ test.ok(false); // waiting for message
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(true);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'Set envelope fails for receiver': function(test) {
+ test.expect(2);
+
+ var client = simplesmtp.connect(PORT_NUMBER, false, {});
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for envelope
+
+ client.useEnvelope({
+ from: 'test at pangalink.net',
+ to: [
+ 'test1 at kreata.ee',
+ 'test2 at kreata.ee'
+ ]
+ });
+ });
+
+ client.on('message', function() {
+ // Client is ready to take messages
+ test.ok(false); // waiting for message
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(true);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'Set envelope partly fails': function(test) {
+ test.expect(3);
+
+ var client = simplesmtp.connect(PORT_NUMBER, false, {});
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for envelope
+
+ client.useEnvelope({
+ from: 'test at pangalink.net',
+ to: [
+ 'test1 at pangalink.net',
+ 'test2 at kreata.ee'
+ ]
+ });
+ });
+
+ client.on('rcptFailed', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for message
+ });
+
+ client.on('message', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for message
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'Send message success': function(test) {
+ test.expect(3);
+
+ var client = simplesmtp.connect(PORT_NUMBER, false, {});
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for envelope
+
+ client.useEnvelope({
+ from: 'test at pangalink.net',
+ to: [
+ 'test1 at pangalink.net',
+ 'test2 at pangalink.net'
+ ]
+ });
+ });
+
+ client.on('message', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for message
+
+ client.write('From: abc at pangalink.net\r\nTo:cde at pangalink.net\r\nSubject: test\r\n\r\nHello World!');
+ client.end();
+ });
+
+ client.on('ready', function(success) {
+ test.ok(success);
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ },
+
+ 'Stream message': function(test) {
+ test.expect(3);
+
+ var client = simplesmtp.connect(PORT_NUMBER, false, {});
+
+ client.once('idle', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for envelope
+
+ client.useEnvelope({
+ from: 'test at pangalink.net',
+ to: [
+ 'test1 at pangalink.net',
+ 'test2 at pangalink.net'
+ ]
+ });
+ });
+
+ client.on('message', function() {
+ // Client is ready to take messages
+ test.ok(true); // waiting for message
+
+ // pipe file to client
+ fs.createReadStream(__dirname + '/testmessage.eml').pipe(client);
+ });
+
+ client.on('ready', function(success) {
+ test.ok(success);
+ client.close();
+ });
+
+ client.on('error', function() {
+ test.ok(false);
+ });
+
+ client.on('end', function() {
+ test.done();
+ });
+ }
+};
\ No newline at end of file
diff --git a/test/pool.js b/test/pool.js
new file mode 100644
index 0000000..663a471
--- /dev/null
+++ b/test/pool.js
@@ -0,0 +1,400 @@
+'use strict';
+
+/* jshint loopfunc: true */
+
+var simplesmtp = require('../index'),
+ MailComposer = require('mailcomposer').MailComposer;
+
+var PORT_NUMBER = 8397;
+
+exports['General tests'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer({});
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Send single message': function(test) {
+
+ var pool = simplesmtp.createClientPool(PORT_NUMBER),
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+
+ mc.setMessageOption({
+ from: 'andmekala at hot.ee',
+ to: 'andris at pangalink.net',
+ subject: 'Hello!',
+ body: 'Hello world!',
+ html: '<b>Hello world!</b>'
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ test.ok(true);
+ callback();
+ });
+
+ pool.sendMail(mc, function(error) {
+ test.ifError(error);
+ pool.close(function() {
+ test.ok(true);
+ test.done();
+ });
+ });
+ },
+
+ 'Send several messages': function(test) {
+ var total = 10;
+
+ test.expect(total * 2);
+
+ var pool = simplesmtp.createClientPool(PORT_NUMBER),
+ mc;
+
+ this.server.on('dataReady', function(envelope, callback) {
+ process.nextTick(callback);
+ });
+
+ var completed = 0;
+ for (var i = 0; i < total; i++) {
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+ mc.setMessageOption({
+ from: 'andmekala at hot.ee',
+ to: 'andris at pangalink.net',
+ subject: 'Hello!',
+ body: 'Hello world!',
+ html: '<b>Hello world!</b>'
+ });
+ pool.sendMail(mc, function(error) {
+ test.ifError(error);
+ test.ok(true);
+ completed++;
+ if (completed >= total) {
+ pool.close(function() {
+ test.done();
+ });
+ }
+ });
+ }
+ },
+
+ 'Delivery error once': function(test) {
+
+ var pool = simplesmtp.createClientPool(PORT_NUMBER),
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+
+ mc.setMessageOption({
+ from: 'andmekala at hot.ee',
+ to: 'andris at pangalink.net',
+ subject: 'Hello!',
+ body: 'Hello world!',
+ html: '<b>Hello world!</b>'
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ test.ok(true);
+ callback(new Error('Spam!'));
+ });
+
+ pool.sendMail(mc, function(error) {
+ test.equal(error && error.name, 'DeliveryError');
+ pool.close(function() {
+ test.ok(true);
+ test.done();
+ });
+ });
+ },
+
+ 'Delivery error several times': function(test) {
+ var total = 10;
+
+ test.expect(total);
+
+ var pool = simplesmtp.createClientPool(PORT_NUMBER),
+ mc;
+
+ this.server.on('dataReady', function(envelope, callback) {
+ process.nextTick(function() {
+ callback(new Error('Spam!'));
+ });
+ });
+
+ var completed = 0;
+ for (var i = 0; i < total; i++) {
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+ mc.setMessageOption({
+ from: 'andmekala at hot.ee',
+ to: 'andris at pangalink.net',
+ subject: 'Hello!',
+ body: 'Hello world!',
+ html: '<b>Hello world!</b>'
+ });
+
+ pool.sendMail(mc, function(error) {
+ test.equal(error && error.name, 'DeliveryError');
+ completed++;
+ if (completed >= total) {
+ pool.close(function() {
+ test.done();
+ });
+ }
+ });
+ }
+ }
+};
+
+exports['Auth fail tests'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer({
+ requireAuthentication: true
+ });
+
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ this.server.on('authorizeUser', function(envelope, username, password, callback) {
+ callback(null, username == password);
+ });
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Authentication passes once': function(test) {
+ var pool = simplesmtp.createClientPool(PORT_NUMBER, false, {
+ auth: {
+ 'user': 'test',
+ 'pass': 'test'
+ }
+ }),
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+
+ mc.setMessageOption({
+ from: 'andmekala2 at hot.ee',
+ to: 'andris2 at pangalink.net',
+ subject: 'Hello2!',
+ body: 'Hello2 world!',
+ html: '<b>Hello2 world!</b>'
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ test.ok(true);
+ callback();
+ });
+
+ pool.sendMail(mc, function(error) {
+ test.ifError(error);
+ pool.close(function() {
+ test.ok(true);
+ test.done();
+ });
+ });
+
+ },
+
+ 'Authentication error once': function(test) {
+ var pool = simplesmtp.createClientPool(PORT_NUMBER, false, {
+ auth: {
+ 'user': 'test1',
+ 'pass': 'test2'
+ }
+ }),
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+
+ mc.setMessageOption({
+ from: 'andmekala2 at hot.ee',
+ to: 'andris2 at pangalink.net',
+ subject: 'Hello2!',
+ body: 'Hello2 world!',
+ html: '<b>Hello2 world!</b>'
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ test.ok(true);
+ callback();
+ });
+
+ pool.sendMail(mc, function(error) {
+ test.equal(error && error.name, 'AuthError');
+ pool.close(function() {
+ test.ok(true);
+ test.done();
+ });
+ });
+ }
+};
+
+exports['Max messages'] = {
+ setUp: function(callback) {
+ this.server = new simplesmtp.createServer({});
+ this.server.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+
+ tearDown: function(callback) {
+ this.server.end(callback);
+ },
+
+ 'Limit 1': function(test) {
+ var total = 10;
+
+ test.expect(total * 3);
+
+ var pool = simplesmtp.createClientPool(PORT_NUMBER, false, {
+ maxMessages: 1,
+ maxConnections: 1
+ }),
+ mc;
+
+ pool.on('released', function() {
+ test.ok(1);
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ process.nextTick(callback);
+ });
+
+ var completed = 0;
+ for (var i = 0; i < total; i++) {
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+ mc.setMessageOption({
+ from: 'andmekala at hot.ee',
+ to: 'andris at pangalink.net',
+ subject: 'Hello!',
+ body: 'Hello world!',
+ html: '<b>Hello world!</b>'
+ });
+ pool.sendMail(mc, function(error) {
+ test.ifError(error);
+ test.ok(true);
+ completed++;
+ if (completed >= total) {
+ pool.close(function() {
+ test.done();
+ });
+ }
+ });
+ }
+ },
+
+ 'Limit 2': function(test) {
+ var total = 10;
+
+ test.expect(total * 2 + 5);
+
+ var pool = simplesmtp.createClientPool(PORT_NUMBER, false, {
+ maxMessages: 2,
+ maxConnections: 1
+ }),
+ mc;
+
+ pool.on('released', function() {
+ test.ok(1);
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ process.nextTick(callback);
+ });
+
+ var completed = 0;
+ for (var i = 0; i < total; i++) {
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+ mc.setMessageOption({
+ from: 'andmekala at hot.ee',
+ to: 'andris at pangalink.net',
+ subject: 'Hello!',
+ body: 'Hello world!',
+ html: '<b>Hello world!</b>'
+ });
+ pool.sendMail(mc, function(error) {
+ test.ifError(error);
+ test.ok(true);
+ completed++;
+ if (completed >= total) {
+ pool.close(function() {
+ test.done();
+ });
+ }
+ });
+ }
+ },
+
+ 'No limit': function(test) {
+ var total = 10;
+
+ test.expect(total * 2);
+
+ var pool = simplesmtp.createClientPool(PORT_NUMBER, false, {
+ maxConnections: 1
+ }),
+ mc;
+
+ pool.on('released', function() {
+ test.ok(1);
+ });
+
+ this.server.on('dataReady', function(envelope, callback) {
+ process.nextTick(callback);
+ });
+
+ var completed = 0;
+ for (var i = 0; i < total; i++) {
+ mc = new MailComposer({
+ escapeSMTP: true
+ });
+ mc.setMessageOption({
+ from: 'andmekala at hot.ee',
+ to: 'andris at pangalink.net',
+ subject: 'Hello!',
+ body: 'Hello world!',
+ html: '<b>Hello world!</b>'
+ });
+ pool.sendMail(mc, function(error) {
+ test.ifError(error);
+ test.ok(true);
+ completed++;
+ if (completed >= total) {
+ pool.close(function() {
+ test.done();
+ });
+ }
+ });
+ }
+ }
+};
\ No newline at end of file
diff --git a/test/server.js b/test/server.js
new file mode 100644
index 0000000..d19877f
--- /dev/null
+++ b/test/server.js
@@ -0,0 +1,739 @@
+'use strict';
+
+var runClientMockup = require('rai').runClientMockup,
+ simplesmtp = require('../index'),
+ netlib = require('net');
+
+var PORT_NUMBER = 8397;
+
+// monkey patch net and tls to support nodejs 0.4
+if (!netlib.connect && netlib.createConnection) {
+ netlib.connect = netlib.createConnection;
+}
+
+exports['General tests'] = {
+ setUp: function(callback) {
+
+ this.smtp = new simplesmtp.createServer({
+ SMTPBanner: 'SCORPIO',
+ name: 'MYRDO',
+ maxSize: 1234,
+ maxClients: 1,
+ });
+ this.smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+ tearDown: function(callback) {
+ this.smtp.end(callback);
+ },
+ 'QUIT': function(test) {
+ var cmds = ['QUIT'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('221', resp.toString('utf-8').trim().substr(0, 3));
+ test.done();
+ });
+
+ },
+ 'HELO': function(test) {
+ var cmds = ['HELO FOO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('250', resp.toString('utf-8').trim().substr(0, 3));
+ test.done();
+ });
+
+ },
+ 'HELO fails': function(test) {
+ var cmds = ['HELO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'EHLO': function(test) {
+ var cmds = ['EHLO FOO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ resp = resp.toString('utf-8').trim();
+ var lines = resp.split('\r\n');
+ for (var i = 0; i < lines.length - 1; i++) {
+ test.equal('250-', lines[i].substr(0, 4));
+ }
+ test.equal('250 ', lines[i].substr(0, 4));
+ test.done();
+ });
+ },
+ 'EHLO fails': function(test) {
+ var cmds = ['EHLO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'HELO after STARTTLS': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'HELO FOO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('250', resp.toString('utf-8').trim().substr(0, 3));
+ test.done();
+ });
+ },
+ 'HELO fails after STARTTLS': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'HELO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'EHLO after STARTTLS': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'HELO FOO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ resp = resp.toString('utf-8').trim();
+ var lines = resp.split('\r\n');
+ for (var i = 0; i < lines.length - 1; i++) {
+ test.equal('250-', lines[i].substr(0, 4));
+ }
+ test.equal('250 ', lines[i].substr(0, 4));
+ test.done();
+ });
+ },
+ 'EHLO fails after STARTTLS': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH fails if not required': function(test) {
+ var cmds = ['EHLO FOO', 'AUTH LOGIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH fails if not required TLS': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'AUTH LOGIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'Custom Greeting banner': function(test) {
+ var client = netlib.connect(PORT_NUMBER, function() {
+ client.on('data', function(chunk) {
+ test.equal('SCORPIO', (chunk || '').toString().trim().split(' ').pop());
+ client.end();
+ });
+ client.on('end', function() {
+ test.done();
+ });
+ });
+ },
+ 'HELO name': function(test) {
+ var cmds = ['HELO FOO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('MYRDO', resp.toString('utf-8').trim().substr(4).split(' ').shift());
+ test.done();
+ });
+ },
+ 'EHLO name': function(test) {
+ var cmds = ['EHLO FOO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('MYRDO', resp.toString('utf-8').trim().substr(4).split(' ').shift());
+ test.done();
+ });
+ },
+ 'MAIL FROM options': function(test) {
+ var cmds = ['HELO FOO', 'MAIL FROM:<test at gmail.com> BODY=8BITMIME'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.ok(resp.toString('utf-8').match(/^250/));
+ test.done();
+ });
+ },
+ 'MAXSIZE': function(test) {
+ var cmds = ['EHLO FOO'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.ok(resp.toString('utf-8').trim().match(/^250[\- ]SIZE 1234$/mi));
+ test.done();
+ });
+ }
+ /*,
+
+ // test disabled due to race conditions (returned false positives if connections are created in non expected order)
+ 'Max Incoming Connections': function(test) {
+ var maxClients = this.smtp.options.maxClients,
+ name = this.smtp.options.name;
+
+ for (var i = 0; i <= maxClients; i++) {
+ runClientMockup(PORT_NUMBER, 'localhost', [], (function(i) {
+ return function(resp) {
+ if (i < maxClients) return;
+ test.ok((new RegExp('^421\\s+' + name)).test(resp.toString('utf-8').trim()));
+ test.done();
+ }
+ })(i));
+ }
+ },
+ */
+};
+
+exports['EHLO setting'] = {
+ setUp: function(callback) {
+
+ this.smtp = new simplesmtp.createServer({
+ disableEHLO: true
+ });
+ this.smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ },
+ tearDown: function(callback) {
+ this.smtp.end(callback);
+ },
+ 'Disable EHLO': function(test) {
+ runClientMockup(PORT_NUMBER, 'localhost', ['EHLO foo'], function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ runClientMockup(PORT_NUMBER, 'localhost', ['HELO foo'], function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ });
+
+ }
+};
+
+exports['Client disconnect'] = {
+
+ 'Client disconnect': function(test) {
+
+ var smtp = new simplesmtp.createServer(),
+ clientEnvelope;
+ smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ }
+
+ runClientMockup(PORT_NUMBER, 'localhost', ['EHLO foo', 'MAIL FROM:<andris at pangalink.net>', 'RCPT TO:<andris at pangalink.net>', 'DATA'], function(resp) {
+ test.equal('3', resp.toString('utf-8').trim().substr(0, 1));
+ });
+
+ });
+ smtp.on('startData', function(envelope) {
+ clientEnvelope = envelope;
+ });
+ smtp.on('close', function(envelope) {
+ test.equal(envelope, clientEnvelope);
+ smtp.end(function() {});
+ test.done();
+ });
+
+ }
+};
+
+exports['Require AUTH'] = {
+ setUp: function(callback) {
+
+ this.smtp = new simplesmtp.createServer({
+ requireAuthentication: true,
+ authMethods: ['PLAIN', 'LOGIN', 'XOAUTH2']
+ });
+ this.smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ this.smtp.on('authorizeUser', function(envelope, username, password, callback) {
+ callback(null, username == 'andris' && password == 'test');
+ });
+
+ },
+ tearDown: function(callback) {
+ this.smtp.end(callback);
+ },
+ 'Fail without AUTH': function(test) {
+ var cmds = ['EHLO FOO', 'MAIL FROM:<andris at pangalink.net>'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'Unknown AUTH': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH CRAM'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH fails before STARTTLS': function(test) {
+ var cmds = ['EHLO FOO', 'AUTH LOGIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('3', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Invalid login': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('inv').toString('base64'),
+ new Buffer('alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Invalid username': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('inv').toString('base64'),
+ new Buffer('test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Invalid password': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('andris').toString('base64'),
+ new Buffer('alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Login success': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('andris').toString('base64'),
+ new Buffer('test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Login with username': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO',
+ 'AUTH LOGIN ' + new Buffer('andris').toString('base64'),
+ new Buffer('test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Login with username - invalid username': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO',
+ 'AUTH LOGIN ' + new Buffer('inv').toString('base64'),
+ new Buffer('test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Login with username - invalid password': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO',
+ 'AUTH LOGIN ' + new Buffer('andris').toString('base64'),
+ new Buffer('inv').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('3', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Invalid login': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('inv\u0000inv\u0000alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Invalid user': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('inv\u0000inv\u0000test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Invalid password': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('andris\u0000andris\u0000alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Login success': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('andris\u0000andris\u0000test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Yet another login success': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN',
+ new Buffer('andris\u0000andris\u0000test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH XOAUTH2 Login success': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH XOAUTH2 ' +
+ new Buffer([
+ 'user=andris',
+ 'auth=Bearer test',
+ '',
+ '\n'
+ ].join('\x01')).toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH XOAUTH2 Login fail': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH XOAUTH2 ' +
+ new Buffer([
+ 'user=andris',
+ 'auth=Bearer test2',
+ '',
+ '\n'
+ ].join('\x01')).toString('base64'),
+ ''
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ }
+};
+
+exports['Enable AUTH'] = {
+ setUp: function(callback) {
+
+ this.smtp = new simplesmtp.createServer({
+ enableAuthentication: true
+ });
+ this.smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ this.smtp.on('authorizeUser', function(envelope, username, password, callback) {
+ callback(null, username == 'andris' && password == 'test');
+ });
+
+ },
+ tearDown: function(callback) {
+ this.smtp.end(callback);
+ },
+ 'Pass without AUTH': function(test) {
+ var cmds = ['EHLO FOO', 'MAIL FROM:<andris at pangalink.net>'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'Unknown AUTH': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH CRAM'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH fails before STARTTLS': function(test) {
+ var cmds = ['EHLO FOO', 'AUTH LOGIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('3', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Invalid login': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('inv').toString('base64'),
+ new Buffer('alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Invalid username': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('inv').toString('base64'),
+ new Buffer('test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Invalid password': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('andris').toString('base64'),
+ new Buffer('alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH LOGIN Login success': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN',
+ new Buffer('andris').toString('base64'),
+ new Buffer('test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('3', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Invalid login': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('inv\u0000inv\u0000alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Invalid user': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('inv\u0000inv\u0000test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Invalid password': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('andris\u0000andris\u0000alid').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Login success': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' +
+ new Buffer('andris\u0000andris\u0000test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'AUTH PLAIN Yet another login success': function(test) {
+ var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN',
+ new Buffer('andris\u0000andris\u0000test').toString('base64')
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ }
+};
+
+exports.ignoreTLS = {
+ setUp: function(callback) {
+
+ this.smtp = new simplesmtp.createServer({
+ requireAuthentication: true,
+ ignoreTLS: true
+ });
+ this.smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ this.smtp.on('authorizeUser', function(envelope, username, password, callback) {
+ callback(null, username == 'd3ph' && password == 'test');
+ });
+ },
+ tearDown: function(callback) {
+ this.smtp.end(callback);
+ },
+ 'Fail without AUTH': function(test) {
+ var cmds = ['EHLO FOO', 'MAIL FROM:<d3ph.ru at gmail.com>'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'Fail MAIL FROM without HELO': function(test) {
+ var cmds = ['MAIL FROM:<d3ph.ru at gmail.com>'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('5', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ },
+ 'Success AUTH & SEND MAIL with <CR><LF>.<CR><LF>': function(test) {
+ var cmds = ['EHLO FOO',
+ 'AUTH PLAIN',
+ new Buffer('\u0000d3ph\u0000test').toString('base64'),
+ 'MAIL FROM:<d3ph at github.com>',
+ 'RCPT TO:<andris at pangalink.net>',
+ 'DATA',
+ 'Test mail\r\n.\r\n',
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ resp = resp.toString('utf-8').trim();
+ test.equal('2', resp.substr(0, 1));
+ test.ok(resp.match('queued as'));
+ test.done();
+ });
+ }
+};
+
+exports['Sending mail listen for dataReady'] = {
+ setUp: function(callback) {
+ var data = '';
+
+ this.smtp = new simplesmtp.createServer({
+ ignoreTLS: true
+ });
+ this.smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ this.smtp.on('authorizeUser', function(envelope, username, password, callback) {
+ callback(null, username == 'd3ph' && password == 'test');
+ });
+
+ this.smtp.on('data', function(envelope, chunk) {
+ data += chunk;
+ });
+
+ this.smtp.on('dataReady', function(envelope, callback) {
+ setTimeout(function() {
+ if (data.match('spam')) {
+ callback(new Error('FAILED'));
+ } else {
+ callback(null, '#ID');
+ }
+ }, 2000);
+ });
+ },
+ tearDown: function(callback) {
+ this.smtp.end(callback);
+ },
+ 'Fail send mail if body contains "spam"': function(test) {
+ var cmds = ['EHLO FOO',
+ 'MAIL FROM:<d3ph at github.com>',
+ 'RCPT TO:<andris at pangalink.net>',
+ 'DATA',
+ 'Test mail with spam!\r\n.\r\n',
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('550 FAILED', resp.toString('utf-8').trim());
+ test.done();
+ });
+ },
+ 'Create #ID for mail': function(test) {
+ var cmds = ['EHLO FOO',
+ 'MAIL FROM:<d3ph at github.com>',
+ 'RCPT TO:<andris at pangalink.net>',
+ 'DATA',
+ 'Clear mail body\r\n.\r\n',
+ ];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ resp = resp.toString('utf-8').trim();
+ test.equal('2', resp.substr(0, 1));
+ test.ok(resp.match('#ID'));
+ test.done();
+ });
+ }
+};
+
+exports['Sending mail listen for dataReady'] = {
+ setUp: function(callback) {
+ var data = '';
+
+ this.smtp = new simplesmtp.createServer({
+ ignoreTLS: true,
+ disableDNSValidation: true
+ });
+ this.smtp.listen(PORT_NUMBER, function(err) {
+ if (err) {
+ throw err;
+ } else {
+ callback();
+ }
+ });
+
+ this.smtp.on('data', function(envelope, chunk) {
+ data += chunk;
+ });
+ },
+
+ tearDown: function(callback) {
+ this.smtp.end(callback);
+ },
+
+ 'Allow empty Mail from': function(test) {
+ var cmds = ['EHLO FOO', 'MAIL FROM:<>'];
+ runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) {
+ test.equal('2', resp.toString('utf-8').trim().substr(0, 1));
+ test.done();
+ });
+ }
+};
\ No newline at end of file
diff --git a/test/testmessage.eml b/test/testmessage.eml
new file mode 100644
index 0000000..dff23e5
--- /dev/null
+++ b/test/testmessage.eml
@@ -0,0 +1,5 @@
+From: test at pangalink.net
+To: test at pangalink.net
+Subject: Test
+
+Hello world!
\ No newline at end of file
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-simplesmtp.git
More information about the Pkg-javascript-commits
mailing list