├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── send.js ├── simpleserver.js ├── size.js └── validate-recipient.js ├── index.js ├── lib ├── client.js ├── pool.js ├── server.js └── simpleserver.js ├── package.json └── test ├── client.js ├── pool.js ├── server.js └── testmessage.eml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | test 3 | examples 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - "0.10" 5 | 6 | notifications: 7 | email: 8 | recipients: 9 | - andris@kreata.ee 10 | on_success: change 11 | on_failure: change 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.3.34 2014-01-14 4 | 5 | * Bumped version to 0.3.34 6 | * Fixed a bug with ES6 strict mode (can't set properties to a `false` value) 7 | 8 | ## v0.3.33 2014-09-04 9 | 10 | * Bumped version to 0.3.33 11 | * Added deprecation notice 12 | 13 | ## v0.3.32 2014-05-30 14 | 15 | * Bumped version to 0.3.32 16 | * ignore close if end was already called [004ebaee] 17 | 18 | ## v0.3.30 2014-05-13 19 | 20 | * Bumped version to 0.3.30 21 | * Added .npmignore [a7344b49] 22 | 23 | ## v0.3.29 2014-05-07 24 | 25 | * Bumped version to 0.3.29 26 | * Changed formatting rules, use single quotes instead of double quotes [92b581c8] 27 | * rollback NOOP usage [e47e24bb] 28 | 29 | ## v0.3.28 2014-05-03 30 | 31 | * Bumped version to 0.3.28 32 | * handle errors with NOOP [deb18352] 33 | 34 | ## v0.3.27 2014-04-23 35 | 36 | * Bumped version to 0.3.27 37 | * get tests running in node 0.8, 0.10, 0.11 [9b3f9043..833388d5] 38 | 39 | ## v0.3.26 2014-04-23 40 | 41 | * Bumped version to 0.3.26 42 | * Server: Added support for XOAUTH2 authentication [87b6ed66] 43 | * Client: Use interval NOOPing to keep the connection up [184d8623] 44 | * Client: do not throw if recipients are note set [785a2b09] 45 | 46 | ## v0.3.25 2014-04-16 47 | 48 | * Bumped version to 0.3.25 49 | * disabled server test for max incoming connections [476f8cf5] 50 | * Added socketTimeout option [b83a4838] 51 | * fix invalid tests [cf22d390] 52 | 53 | ## v0.3.24 2014-03-31 54 | 55 | * Bumped version to 0.3.24 56 | * Added test for empty MAIL FROM [7f17174d] 57 | * Allow null return sender in mail command (coxeh) [08bc6a6f] 58 | * incorrect mail format fix (siterra) [d42d364e] 59 | * support for `form` and `to` structure: {address:"...",name:"..."} siterra) [2b054740] 60 | * Improved auth supports detection (finian) [863dc019] 61 | * Fixed a Buffer building bug (finian) [6dc9a4e2] 62 | 63 | ## v0.3.23 2014-03-10 64 | 65 | * Bumped version to 0.3.23 66 | * removed pipelining [4f0a382f] 67 | * Rename disableDotEscaping to enableDotEscaping [5534bd85] 68 | * Ignore OAuth2 errors from destroyed connections (SLaks) [e8ff3356] 69 | 70 | ## v0.3.22 2014-02-16 71 | 72 | * Bumped version to 0.3.22 73 | * Emit error on unexpected close [111da167] 74 | * Allowed persistence of custom properties when resetting envelope state. (garbetjie) [b49b7ead] 75 | 76 | ## v0.3.21 2014-02-16 77 | 78 | * Bumped version to 0.3.21 79 | * Ignore OAuth errors from destroyed connections (SLaks) [d50a7571] 80 | 81 | ## v0.3.20 2014-01-28 82 | 83 | * Bumped version to 0.3.20 84 | * Re-emit 'drain' from tcp socket [5bfb1fcc] 85 | 86 | ## v0.3.19 2014-01-28 87 | 88 | * Bumped version to 0.3.19 89 | * Prefer setImmediate over nextTick if available [f53e2d44] 90 | * Server: Implemented "NOOP" command (codingphil) [707485c0] 91 | * Server: Allow SIZE with MAIL [3b404028] 92 | 93 | ## v0.3.18 2014-01-05 94 | 95 | * Bumped version to 0.3.18 96 | * Added limiting of max client connections (garbetjie) [bcd5c0b3] 97 | 98 | ## v0.3.17 2014-01-05 99 | 100 | * Bumped version to 0.3.17 101 | * Do not create a server instance with invalid socket (47d17420) 102 | * typo (chrisdew) [fe4df83f] 103 | * Only emit rcptFailed if there actually was an address that was rejected [4c75523f] 104 | 105 | ## v0.3.16 2013-12-02 106 | 107 | * Bumped version to 0.3.16 108 | * Expose simplesmtp version number [c2382203] 109 | * typo in SMTP (chrisdew) [6c39a8d7] 110 | * Fix typo in README.md (Meekohi) [597a25cb] 111 | 112 | ## v0.3.15 2013-11-15 113 | 114 | * Bumped version to 0.3.15 115 | * Fixed bugs in connection timeout implementation (finian) [1a25d5af] 116 | 117 | ## v0.3.14 2013-11-08 118 | 119 | * Bumped version to 0.3.14 120 | * fixed: typo causing connection.remoteAddress to be undefined (johnnyleung) 795fe81f 121 | * improvements to handling stage (mysz) 5a79e6a1 122 | * fixes TypeError: Cannot use 'in' operator to search for 'dsn' in undefined (mysz) 388d9b82 123 | * lost saving stage in "DATA" (mysz) de694f67 124 | * more info on smtp error (mysz) 42a4f964 125 | 126 | ## v0.3.13 2013-10-29 127 | 128 | * Bumped version to 0.3.13 129 | * Handling errors which close connection on or before EHLO (mysz) 03345d4d 130 | 131 | ## v0.3.12 2013-10-29 132 | 133 | * Bumped version to 0.3.12 134 | * Allow setting maxMessages to pool 5d185708 135 | 136 | ## v0.3.11 2013-10-22 137 | 138 | * Bumped version to 0.3.11 139 | * style update 2095d3a9 140 | * fix tests 17a3632f 141 | * DSN Support implemented. (irvinzz) d1e8ba29 142 | 143 | ## v0.3.10 2013-09-09 144 | 145 | * Bumped version to 0.3.10 146 | * added greetingTimeout, connectionTimeout and rejectUnathorized options to connection pool 8fa55cd3 147 | 148 | ## v0.3.9 2013-09-09 149 | 150 | * Bumped version to 0.3.9 151 | * added "use strict" definitions, added new options for client: greetingTimeout, connectionTimeout, rejectUnathorized 51047ae0 152 | * Do not include localAddress in the options if it is unset 7eb0e8fc 153 | 154 | ## v0.3.8 2013-08-21 155 | 156 | * Bumped version to 0.3.8 157 | * short fix for #42, Client parser hangs on certain input (dannycoates) 089f5cd4 158 | 159 | ## v0.3.7 2013-08-16 160 | 161 | * Bumped version to 0.3.7 162 | * minor adjustments for better portability with browserify (whiteout-io) 15715498 163 | * Added raw message to error object (andremetzen) 15715498 164 | * Passing to error handler the message sent from SMTP server when an error occurred (andremetzen) 15d4cbb4 165 | 166 | ## v0.3.6 2013-08-06 167 | 168 | * Bumped version to 0.3.6 169 | * Added changelog 170 | * Timeout if greeting is not received after connection is established -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simplesmtp 2 | 3 | ## DEPRECATION NOTICE 4 | 5 | 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). 6 | 7 | -------- 8 | 9 | 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. 10 | 11 | Should be fine though for integration testing purposes. 12 | 13 | ## Info 14 | 15 | This is a module to easily create custom SMTP servers and clients - use SMTP as a first class protocol in Node.JS! 16 | 17 | [![Build Status](https://secure.travis-ci.org/andris9/simplesmtp.png)](http://travis-ci.org/andris9/simplesmtp) 18 | [![NPM version](https://badge.fury.io/js/simplesmtp.png)](http://badge.fury.io/js/simplesmtp) 19 | 20 | ## Version warning! 21 | 22 | If you are using node v0.6, then the last usable version of **simplesmtp** is v0.2.7 23 | 24 | Current version of simplesmtp is fully supported for Node v0.8+ 25 | 26 | ˇ## SMTP Server 27 | 28 | ## Simple SMTP server 29 | 30 | For a simple inbound only, no authentication SMTP server you can use 31 | 32 | simplesmtp.createSimpleServer([options], requestListener).listen(port); 33 | 34 | Example 35 | 36 | simplesmtp.createSimpleServer({SMTPBanner:"My Server"}, function(req){ 37 | req.pipe(process.stdout); 38 | req.accept(); 39 | }).listen(port); 40 | 41 | Properties 42 | 43 | * **req.from** - From address 44 | * **req.to** - an array of To addresses 45 | * **req.host** - hostname reported by the client 46 | * **req.remodeAddress** - client IP address 47 | 48 | Methods 49 | 50 | * **req.accept** *([id])* - Accept the message with the selected ID 51 | * **req.reject** *([message])* - Reject the message with the selected message 52 | * **req.pipe** *(stream)* - Pipe the incoming data to a writable stream 53 | 54 | Events 55 | 56 | * **'data'** *(chunk)* - A chunk (Buffer) of the message. 57 | * **'end'** - The message has been transferred 58 | 59 | 60 | ## Advanced SMTP server 61 | 62 | ### Usage 63 | 64 | Create a new SMTP server instance with 65 | 66 | var smtp = simplesmtp.createServer([options]); 67 | 68 | And start listening on selected port 69 | 70 | smtp.listen(25, [function(err){}]); 71 | 72 | SMTP options can include the following: 73 | 74 | * **name** - the hostname of the server, will be used for informational messages 75 | * **debug** - if set to true, print out messages about the connection 76 | * **timeout** - client timeout in milliseconds, defaults to 60 000 (60 sec.) 77 | * **secureConnection** - start a server on secure connection 78 | * **SMTPBanner** - greeting banner that is sent to the client on connection 79 | * **requireAuthentication** - if set to true, require that the client must authenticate itself 80 | * **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) 81 | * **maxSize** - maximum size of an e-mail in bytes (currently informational only) 82 | * **credentials** - TLS credentials (`{key:'', cert:'', ca:['']}`) for the server 83 | * **authMethods** - allowed authentication methods, defaults to `["PLAIN", "LOGIN"]` 84 | * **disableEHLO** - if set to true, support HELO command only 85 | * **ignoreTLS** - if set to true, allow client do not use STARTTLS 86 | * **disableDNSValidation** - if set, do not validate sender domains 87 | * **disableSTARTTLS** - if set, do not use STARTTLS 88 | 89 | ### Example 90 | 91 | var simplesmtp = require("simplesmtp"), 92 | fs = require("fs"); 93 | 94 | var smtp = simplesmtp.createServer(); 95 | smtp.listen(25); 96 | 97 | smtp.on("startData", function(connection){ 98 | console.log("Message from:", connection.from); 99 | console.log("Message to:", connection.to); 100 | connection.saveStream = fs.createWriteStream("/tmp/message.txt"); 101 | }); 102 | 103 | smtp.on("data", function(connection, chunk){ 104 | connection.saveStream.write(chunk); 105 | }); 106 | 107 | smtp.on("dataReady", function(connection, callback){ 108 | connection.saveStream.end(); 109 | console.log("Incoming message saved to /tmp/message.txt"); 110 | callback(null, "ABC1"); // ABC1 is the queue id to be advertised to the client 111 | // callback(new Error("Rejected as spam!")); // reported back to the client 112 | }); 113 | 114 | ### Events 115 | 116 | * **startData** *(connection)* - DATA stream is opened by the client (`connection` is an object with `from`, `to`, `host` and `remoteAddress` properties) 117 | * **data** *(connection, chunk)* - e-mail data chunk is passed from the client 118 | * **dataReady** *(connection, callback)* - client is finished passing e-mail data, `callback` returns the queue id to the client 119 | * **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 120 | * **validateSender** *(connection, email, callback)* - will be emitted if `validateSender` listener is set up 121 | * **validateRecipient** *(connection, email, callback)* - will be emitted it `validataRecipients` listener is set up 122 | * **close** *(connection)* - emitted when the connection to client is closed 123 | 124 | ## SMTP Client 125 | 126 | ### Usage 127 | 128 | SMTP client can be created with `simplesmtp.connect(port[,host][, options])` 129 | where 130 | 131 | * **port** is the port to connect to 132 | * **host** is the hostname to connect to (defaults to "localhost") 133 | * **options** is an optional options object (see below) 134 | 135 | ### Connection options 136 | 137 | The following connection options can be used with `simplesmtp.connect`: 138 | 139 | * **secureConnection** - use SSL 140 | * **name** - the name of the client server 141 | * **auth** - authentication object `{user:"...", pass:"..."}` or `{XOAuthToken:"base64data"}` 142 | * **ignoreTLS** - ignore server support for STARTTLS 143 | * **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}` 144 | * **debug** - output client and server messages to console 145 | * **logFile** - optional filename where communication with remote server has to be logged 146 | * **instanceId** - unique instance id for debugging (will be output console with the messages) 147 | * **localAddress** - local interface to bind to for network connections (needs Node.js >= 0.11.3 for working with tls) 148 | * **greetingTimeout** (defaults to 10000) - Time to wait in ms until greeting message is received from the server 149 | * **connectionTimeout** (system default if not set) - Time to wait in ms until the socket is opened to the server 150 | * **socketTimeout** (defaults to 1 hour) - Time of inactivity until the connection is closed 151 | * **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 152 | * **dsn** - An object with methods `success`, `failure` and `delay`. If any of these are set to true, DSN will be used 153 | * **enableDotEscaping** set to true if you want to escape dots at the begining of each line. Defaults to false. 154 | 155 | ### Connection events 156 | 157 | Once a connection is set up the following events can be listened to: 158 | 159 | * **'idle'** - the connection to the SMTP server has been successfully set up and the client is waiting for an envelope 160 | * **'message'** - the envelope is passed successfully to the server and a message stream can be started 161 | * **'ready'** `(success)` - the message was sent 162 | * **'rcptFailed'** `(addresses)` - not all recipients were accepted (invalid addresses are included as an array) 163 | * **'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. 164 | * **'end'** - connection to the client is closed 165 | 166 | ### Sending an envelope 167 | 168 | When an `'idle'` event is emitted, an envelope object can be sent to the server. 169 | This includes a string `from` and an array of strings `to` property. 170 | 171 | Envelope can be sent with `client.useEnvelope(envelope)` 172 | 173 | // run only once as 'idle' is emitted again after message delivery 174 | client.once("idle", function(){ 175 | client.useEnvelope({ 176 | from: "me@example.com", 177 | to: ["receiver1@example.com", "receiver2@example.com"] 178 | }); 179 | }); 180 | 181 | The `to` part of the envelope includes **all** recipients from `To:`, `Cc:` and `Bcc:` fields. 182 | 183 | If setting the envelope up fails, an error is emitted. If only some (not all) 184 | recipients are not accepted, the mail can still be sent but an `rcptFailed` 185 | event is emitted. 186 | 187 | client.on("rcptFailed", function(addresses){ 188 | console.log("The following addresses were rejected: ", addresses); 189 | }); 190 | 191 | If the envelope is set up correctly a `'message'` event is emitted. 192 | 193 | ### Sending a message 194 | 195 | When `'message'` event is emitted, it is possible to send mail. To do this 196 | you can pipe directly a message source (for example an .eml file) to the client 197 | or alternatively you can send the message with `client.write` calls (you also 198 | need to call `client.end()` once the message is completed. 199 | 200 | If you are piping a stream to the client, do not leave the `'end'` event out, 201 | this is needed to complete the message sequence by the client. 202 | 203 | client.on("message", function(){ 204 | fs.createReadStream("test.eml").pipe(client); 205 | }); 206 | 207 | Once the message is delivered a `'ready'` event is emitted. The event has an 208 | parameter which indicates if the message was transmitted( (true) or not (false) 209 | and another which includes the last received data from the server. 210 | 211 | client.on("ready", function(success, response){ 212 | if(success){ 213 | console.log("The message was transmitted successfully with "+response); 214 | } 215 | }); 216 | 217 | ### XOAUTH 218 | 219 | **simplesmtp** supports [XOAUTH2 and XOAUTH](https://developers.google.com/google-apps/gmail/oauth_protocol) authentication. 220 | 221 | #### XOAUTH2 222 | 223 | To use this feature you can set `XOAuth2` param as an `auth` option 224 | 225 | var mailOptions = { 226 | ..., 227 | auth:{ 228 | XOAuth2: { 229 | user: "example.user@gmail.com", 230 | clientId: "8819981768.apps.googleusercontent.com", 231 | clientSecret: "{client_secret}", 232 | refreshToken: "1/xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI", 233 | accessToken: "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==", 234 | timeout: 3600 235 | } 236 | } 237 | } 238 | 239 | `accessToken` and `timeout` values are optional. If login fails a new access token is generated automatically. 240 | 241 | #### XOAUTH 242 | 243 | To use this feature you can set `XOAuthToken` param as an `auth` option 244 | 245 | var mailOptions = { 246 | ..., 247 | auth:{ 248 | XOAuthToken: "R0VUIGh0dHBzOi8vbWFpbC5nb29...." 249 | } 250 | } 251 | 252 | Alternatively it is also possible to use XOAuthToken generators (supported by Nodemailer) - this 253 | needs to be an object with a mandatory method `generate` that takes a callback function for 254 | generating a XOAUTH token string. This is better for generating tokens only when needed - 255 | there is no need to calculate unique token for every e-mail request, since a lot of these 256 | might share the same connection and thus the cleint needs not to re-authenticate itself 257 | with another token. 258 | 259 | var XOGen = { 260 | token: "abc", 261 | generate: function(callback){ 262 | if(1 != 1){ 263 | return callback(new Error("Tokens can't be generated in strange environments")); 264 | } 265 | callback(null, new Buffer(this.token, "utf-8").toString("base64")); 266 | } 267 | } 268 | 269 | var mailOptions = { 270 | ..., 271 | auth:{ 272 | XOAuthToken: XOGen 273 | } 274 | } 275 | 276 | ### Error types 277 | 278 | Emitted errors include the reason for failing in the `name` property 279 | 280 | * **UnknowAuthError** - the client tried to authenticate but the method was not supported 281 | * **AuthError** - the username/password used were rejected 282 | * **TLSError** - STARTTLS failed 283 | * **SenderError** - the sender e-mail address was rejected 284 | * **RecipientError** - all recipients were rejected (if only some of the recipients are rejected, a `'rcptFailed'` event is raised instead 285 | 286 | There's also an additional property in the error object called `data` that includes 287 | the last response received from the server (if available for the current error type). 288 | 289 | ### About reusing the connection 290 | 291 | You can reuse the same connection several times but you can't send a mail 292 | through the same connection concurrently. So if you catch and `'idle'` event 293 | lock the connection to a message process and unlock after `'ready'`. 294 | 295 | On `'error'` events you should reschedule the message and on `'end'` events 296 | you should recreate the connection. 297 | 298 | ### Closing the client 299 | 300 | By default the client tries to keep the connection up. If you want to close it, 301 | run `client.quit()` - this sends a `QUIT` command to the server and closes the 302 | connection 303 | 304 | client.quit(); 305 | 306 | ## SMTP Client Connection pool 307 | 308 | **simplesmtp** has the option for connection pooling if you want to reuse a bulk 309 | of connections. 310 | 311 | ### Usage 312 | 313 | Create a connection pool of SMTP clients with 314 | 315 | simplesmtp.createClientPool(port[,host][, options]) 316 | 317 | where 318 | 319 | * **port** is the port to connect to 320 | * **host** is the hostname to connect to (defaults to "localhost") 321 | * **options** is an optional options object (see below) 322 | 323 | ### Connection options 324 | 325 | The following connection options can be used with `simplesmtp.connect`: 326 | 327 | * **secureConnection** - use SSL 328 | * **name** - the name of the client server 329 | * **auth** - authentication object `{user:"...", pass:"..."}` or `{XOAuthToken:"base64data"}` 330 | * **ignoreTLS** - ignore server support for STARTTLS 331 | * **debug** - output client and server messages to console 332 | * **logFile** - optional filename where communication with remote server has to be logged 333 | * **maxConnections** - how many connections to keep in the pool (defaults to 5) 334 | * **localAddress** - local interface to bind to for network connections (needs Node.js >= 0.11.3 for working with tls) 335 | * **maxMessages** - limit the count of messages to send through a single connection (no limit by default) 336 | 337 | ### Send an e-mail 338 | 339 | E-mails can be sent through the pool with 340 | 341 | pool.sendMail(mail[, callback]) 342 | 343 | where 344 | 345 | * **mail** is a [MailComposer](https://github.com/andris9/mailcomposer) compatible object 346 | * **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. 347 | 348 | ### Errors 349 | 350 | In addition to SMTP client errors another error name is used 351 | 352 | * **DeliveryError** - used if the message was not accepted by the SMTP server 353 | 354 | ## License 355 | 356 | **MIT** 357 | 358 | -------------------------------------------------------------------------------- /examples/send.js: -------------------------------------------------------------------------------- 1 | var simplesmtp = require('../index'); 2 | 3 | mail('sender@example.com', 'receiver@example.com', 'subject: test\r\n\r\nhello world!'); 4 | 5 | /** 6 | * Send a raw email 7 | * 8 | * @param {String} from E-mail address of the sender 9 | * @param {String|Array} to E-mail address or a list of addresses of the receiver 10 | * @param {[type]} message Mime message 11 | */ 12 | function mail(from, to, message) { 13 | var client = simplesmtp.connect(465, 'smtp.gmail.com', { 14 | secureConnection: true, 15 | auth: { 16 | user: 'gmail.username@gmail.com', 17 | pass: 'gmail_pass' 18 | }, 19 | debug: true 20 | }); 21 | 22 | client.once('idle', function() { 23 | client.useEnvelope({ 24 | from: from, 25 | to: [].concat(to || []) 26 | }); 27 | }); 28 | 29 | client.on('message', function() { 30 | client.write(message.replace(/\r?\n/g, '\r\n').replace(/^\./gm, '..')); 31 | client.end(); 32 | }); 33 | 34 | client.on('ready', function(success) { 35 | client.quit(); 36 | }); 37 | 38 | client.on('error', function(err) { 39 | console.log('ERROR'); 40 | console.log(err); 41 | }); 42 | 43 | client.on('end', function() { 44 | console.log('DONE') 45 | }); 46 | } -------------------------------------------------------------------------------- /examples/simpleserver.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | //console.log(process.stdout.writable); 4 | var simplesmtp = require("../index"); 5 | 6 | simplesmtp.createSimpleServer({SMTPBanner:"My Server", debug: true}, function(req){ 7 | process.stdout.write("\r\nNew Mail:\r\n"); 8 | req.on("data", function(chunk){ 9 | process.stdout.write(chunk); 10 | }); 11 | req.accept(); 12 | }).listen(25, function(err){ 13 | if(!err){ 14 | console.log("SMTP server listening on port 25"); 15 | }else{ 16 | console.log("Could not start server on port 25. Ports under 1000 require root privileges."); 17 | console.log(err.message); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /examples/size.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var simplesmtp = require("../index"), 4 | fs = require("fs"); 5 | 6 | // Example for http://tools.ietf.org/search/rfc1870 7 | 8 | var maxMessageSize = 10; 9 | 10 | var smtp = simplesmtp.createServer({ 11 | maxSize: maxMessageSize, // maxSize must be set in order to support SIZE 12 | disableDNSValidation: true, 13 | debug: true 14 | }); 15 | smtp.listen(25); 16 | 17 | // Set up sender validation function 18 | smtp.on("validateSender", function(connection, email, done){ 19 | console.log(1, connection.messageSize, maxMessageSize); 20 | // SIZE value can be found from connection.messageSize 21 | if(connection.messageSize > maxMessageSize){ 22 | var err = new Error("Max space reached"); 23 | err.SMTPResponse = "452 This server can only accept messages up to " + maxMessageSize + " bytes"; 24 | done(err); 25 | }else{ 26 | done(); 27 | } 28 | }); 29 | 30 | // Set up recipient validation function 31 | smtp.on("validateRecipient", function(connection, email, done){ 32 | // Allow only messages up to 100 bytes 33 | if(connection.messageSize > 100){ 34 | var err = new Error("Max space reached"); 35 | err.SMTPResponse = "552 Channel size limit exceeded: " + email; 36 | done(err); 37 | }else{ 38 | done(); 39 | } 40 | }); 41 | 42 | smtp.on("startData", function(connection){ 43 | connection.messageSize = 0; 44 | connection.saveStream = fs.createWriteStream("/tmp/message.txt"); 45 | }); 46 | 47 | smtp.on("data", function(connection, chunk){ 48 | connection.messageSize += chunk.length; 49 | connection.saveStream.write(chunk); 50 | }); 51 | 52 | smtp.on("dataReady", function(connection, done){ 53 | connection.saveStream.end(); 54 | 55 | // check if message 56 | if(connection.messageSize > maxMessageSize){ 57 | // mail was too big and therefore ignored 58 | var err = new Error("Max fileSize reached"); 59 | err.SMTPResponse = "552 message exceeds fixed maximum message size"; 60 | done(err); 61 | }else{ 62 | done(); 63 | console.log("Delivered message by " + connection.from + 64 | " to " + connection.to.join(", ") + ", sent from " + connection.host + 65 | " (" + connection.remoteAddress + ")"); 66 | } 67 | }); -------------------------------------------------------------------------------- /examples/validate-recipient.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var simplesmtp = require("simplesmtp"), 4 | fs = require("fs"); 5 | 6 | var allowedRecipientDomains = ["node.ee", "neti.ee"]; 7 | 8 | var smtp = simplesmtp.createServer(); 9 | smtp.listen(25); 10 | 11 | // Set up recipient validation function 12 | smtp.on("validateRecipient", function(connection, email, done){ 13 | var domain = ((email || "").split("@").pop() || "").toLowerCase().trim(); 14 | 15 | if(allowedRecipientDomains.indexOf(domain) < 0){ 16 | done(new Error("Invalid domain")); 17 | }else{ 18 | done(); 19 | } 20 | }); 21 | 22 | smtp.on("startData", function(connection){ 23 | connection.saveStream = fs.createWriteStream("/tmp/message.txt"); 24 | }); 25 | 26 | smtp.on("data", function(connection, chunk){ 27 | connection.saveStream.write(chunk); 28 | }); 29 | 30 | smtp.on("dataReady", function(connection, done){ 31 | connection.saveStream.end(); 32 | done(); 33 | 34 | console.log("Delivered message by " + connection.from + 35 | " to " + connection.to.join(", ") + ", sent from " + connection.host + 36 | " (" + connection.remoteAddress + ")"); 37 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var packageData = require('./package.json'); 2 | 3 | // expose the API to the world 4 | module.exports.createServer = require('./lib/server.js'); 5 | module.exports.createSimpleServer = require('./lib/simpleserver.js'); 6 | module.exports.connect = require('./lib/client.js'); 7 | module.exports.createClientPool = require('./lib/pool.js'); 8 | module.exports.version = packageData.version; -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = require('stream').Stream, 4 | utillib = require('util'), 5 | net = require('net'), 6 | tls = require('tls'), 7 | oslib = require('os'), 8 | xoauth2 = require('xoauth2'), 9 | crypto = require('crypto'), 10 | fs = require('fs'); 11 | 12 | // expose to the world 13 | module.exports = function(port, host, options) { 14 | var connection = new SMTPClient(port, host, options); 15 | 16 | if (typeof setImmediate == 'function') { 17 | setImmediate(connection.connect.bind(connection)); 18 | } else { 19 | process.nextTick(connection.connect.bind(connection)); 20 | } 21 | 22 | return connection; 23 | }; 24 | 25 | /** 26 | *

Generates a SMTP connection object

27 | * 28 | *

Optional options object takes the following possible properties:

29 | * 42 | * 43 | * @constructor 44 | * @namespace SMTP Client module 45 | * @param {Number} [port=25] Port number to connect to 46 | * @param {String} [host='localhost'] Hostname to connect to 47 | * @param {Object} [options] Option properties 48 | */ 49 | function SMTPClient(port, host, options) { 50 | Stream.call(this); 51 | this.writable = true; 52 | this.readable = true; 53 | 54 | this.stage = 'init'; 55 | 56 | this.options = options || {}; 57 | 58 | this.port = port || (this.options.secureConnection ? 465 : 25); 59 | this.host = host || 'localhost'; 60 | 61 | this.options.secureConnection = !! this.options.secureConnection; 62 | this.options.auth = this.options.auth || false; 63 | this.options.maxConnections = this.options.maxConnections || 5; 64 | this.options.enableDotEscaping = this.options.enableDotEscaping || false; 65 | 66 | this._closing = false; 67 | 68 | if (!this.options.name) { 69 | // defaul hostname is machine hostname or [IP] 70 | var defaultHostname = (oslib.hostname && oslib.hostname()) || ''; 71 | 72 | if (defaultHostname.indexOf('.') < 0) { 73 | defaultHostname = '[127.0.0.1]'; 74 | } 75 | if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { 76 | defaultHostname = '[' + defaultHostname + ']'; 77 | } 78 | 79 | this.options.name = defaultHostname; 80 | } 81 | 82 | this._init(); 83 | } 84 | utillib.inherits(SMTPClient, Stream); 85 | 86 | /** 87 | *

Initializes instance variables

88 | */ 89 | SMTPClient.prototype._init = function() { 90 | /** 91 | * Defines if the current connection is secure or not. If not, 92 | * STARTTLS can be used if available 93 | * @private 94 | */ 95 | this._secureMode = false; 96 | 97 | /** 98 | * Ignore incoming data on TLS negotiation 99 | * @private 100 | */ 101 | this._ignoreData = false; 102 | 103 | /** 104 | * Store incomplete messages coming from the server 105 | * @private 106 | */ 107 | this._remainder = ''; 108 | 109 | /** 110 | * If set to true, then this object is no longer active 111 | * @private 112 | */ 113 | this.destroyed = false; 114 | 115 | /** 116 | * The socket connecting to the server 117 | * @publick 118 | */ 119 | this.socket = false; 120 | 121 | /** 122 | * Lists supported auth mechanisms 123 | * @private 124 | */ 125 | this._supportedAuth = []; 126 | 127 | /** 128 | * Currently in data transfer state 129 | * @private 130 | */ 131 | this._dataMode = false; 132 | 133 | /** 134 | * Keep track if the client sends a leading \r\n in data mode 135 | * @private 136 | */ 137 | this._lastDataBytes = new Buffer(2); 138 | this._lastDataBytes[0] = 0x0D; 139 | this._lastDataBytes[1] = 0x0A; 140 | 141 | 142 | /** 143 | * Function to run if a data chunk comes from the server 144 | * @private 145 | */ 146 | this._currentAction = false; 147 | 148 | /** 149 | * Timeout variable for waiting the greeting 150 | * @private 151 | */ 152 | this._greetingTimeout = false; 153 | 154 | /** 155 | * Timeout variable for waiting the connection to start 156 | * @private 157 | */ 158 | this._connectionTimeout = false; 159 | 160 | if (this.options.ignoreTLS || this.options.secureConnection) { 161 | this._secureMode = true; 162 | } 163 | 164 | /** 165 | * XOAuth2 token generator if XOAUTH2 auth is used 166 | * @private 167 | */ 168 | this._xoauth2 = false; 169 | 170 | if (typeof this.options.auth.XOAuth2 == 'object' && typeof this.options.auth.XOAuth2.getToken == 'function') { 171 | this._xoauth2 = this.options.auth.XOAuth2; 172 | } else if (typeof this.options.auth.XOAuth2 == 'object') { 173 | if (!this.options.auth.XOAuth2.user && this.options.auth.user) { 174 | this.options.auth.XOAuth2.user = this.options.auth.user; 175 | } 176 | this._xoauth2 = xoauth2.createXOAuth2Generator(this.options.auth.XOAuth2); 177 | } 178 | }; 179 | 180 | /** 181 | *

Creates a connection to a SMTP server and sets up connection 182 | * listener

183 | */ 184 | SMTPClient.prototype.connect = function() { 185 | var opts = {}; 186 | if (this.options.secureConnection) { 187 | if (this.options.tls) { 188 | Object.keys(this.options.tls).forEach((function(key) { 189 | opts[key] = this.options.tls[key]; 190 | }).bind(this)); 191 | } 192 | 193 | if (!('rejectUnauthorized' in opts)) { 194 | opts.rejectUnauthorized = !! this.options.rejectUnauthorized; 195 | } 196 | 197 | if (this.options.localAddress) { 198 | opts.localAddress = this.options.localAddress; 199 | } 200 | 201 | this.socket = tls.connect(this.port, this.host, opts, this._onConnect.bind(this)); 202 | } else { 203 | opts = { 204 | port: this.port, 205 | host: this.host 206 | }; 207 | if (this.options.localAddress) { 208 | opts.localAddress = this.options.localAddress; 209 | } 210 | this.socket = net.connect(opts, this._onConnect.bind(this)); 211 | } 212 | 213 | if (this.options.connectionTimeout) { 214 | this._connectionTimeout = setTimeout((function() { 215 | var error = new Error('Connection timeout'); 216 | error.code = 'ETIMEDOUT'; 217 | error.errno = 'ETIMEDOUT'; 218 | error.stage = this.stage; 219 | this.emit('error', error); 220 | this.close(); 221 | }).bind(this), this.options.connectionTimeout); 222 | } 223 | 224 | this.socket.on('drain', this._onDrain.bind(this)); 225 | 226 | this.socket.on('error', this._onError.bind(this)); 227 | }; 228 | 229 | /** 230 | *

Upgrades the connection to TLS

231 | * 232 | * @param {Function} callback Callback function to run when the connection 233 | * has been secured 234 | */ 235 | SMTPClient.prototype._upgradeConnection = function(callback) { 236 | this._ignoreData = true; 237 | this.socket.removeAllListeners('data'); 238 | this.socket.removeAllListeners('error'); 239 | 240 | var opts = { 241 | socket: this.socket, 242 | host: this.host, 243 | rejectUnauthorized: !! this.options.rejectUnauthorized 244 | }; 245 | 246 | Object.keys(this.options.tls || {}).forEach((function(key) { 247 | opts[key] = this.options.tls[key]; 248 | }).bind(this)); 249 | 250 | this.socket = tls.connect(opts, (function() { 251 | this._ignoreData = false; 252 | this._secureMode = true; 253 | this.socket.on('data', this._onData.bind(this)); 254 | 255 | return callback(null, true); 256 | }).bind(this)); 257 | this.socket.on('error', this._onError.bind(this)); 258 | }; 259 | 260 | /** 261 | *

Connection listener that is run when the connection to 262 | * the server is opened

263 | * 264 | * @event 265 | */ 266 | SMTPClient.prototype._onConnect = function() { 267 | this.stage = 'connect'; 268 | 269 | clearTimeout(this._connectionTimeout); 270 | 271 | if ('setKeepAlive' in this.socket) { 272 | this.socket.setKeepAlive(true); 273 | } 274 | 275 | if ('setNoDelay' in this.socket) { 276 | this.socket.setNoDelay(true); 277 | } 278 | 279 | this.socket.on('data', this._onData.bind(this)); 280 | this.socket.on('close', this._onClose.bind(this)); 281 | this.socket.on('end', this._onEnd.bind(this)); 282 | 283 | this.socket.setTimeout(this.options.socketTimeout || (3 * 3600 * 1000)); // 1 hours 284 | this.socket.on('timeout', this._onTimeout.bind(this)); 285 | 286 | this._greetingTimeout = setTimeout((function() { 287 | // if still waiting for greeting, give up 288 | if (this.socket && !this._destroyed && this._currentAction == this._actionGreeting) { 289 | var error = new Error('Greeting never received'); 290 | error.code = 'ETIMEDOUT'; 291 | error.errno = 'ETIMEDOUT'; 292 | error.stage = this.stage; 293 | this.emit('error', error); 294 | this.close(); 295 | } 296 | }).bind(this), this.options.greetingTimeout || 10000); 297 | 298 | this._currentAction = this._actionGreeting; 299 | }; 300 | 301 | /** 302 | *

Destroys the client - removes listeners etc.

303 | */ 304 | SMTPClient.prototype._destroy = function() { 305 | if (this._destroyed) { 306 | return; 307 | } 308 | this._destroyed = true; 309 | this._ignoreData = true; 310 | this.emit('end'); 311 | this.removeAllListeners(); 312 | // keep the error handler around, just in case 313 | this.socket.on('error', this._onError.bind(this)); 314 | }; 315 | 316 | /** 317 | *

'data' listener for data coming from the server

318 | * 319 | * @event 320 | * @param {Buffer} chunk Data chunk coming from the server 321 | */ 322 | SMTPClient.prototype._onData = function(chunk) { 323 | var str; 324 | 325 | if (this._ignoreData || !chunk || !chunk.length) { 326 | return; 327 | } 328 | 329 | // Wait until end of line 330 | if (chunk.readUInt8(chunk.length - 1) != 0x0A) { 331 | this._remainder += chunk.toString(); 332 | return; 333 | } else { 334 | str = (this._remainder + chunk.toString()).trim(); 335 | this._remainder = ''; 336 | } 337 | 338 | // if this is a multi line reply, wait until the ending 339 | if (str.match(/(?:^|\n)\d{3}-.+$/)) { 340 | this._remainder = str + '\r\n'; 341 | return; 342 | } 343 | 344 | if (this.options.debug) { 345 | console.log('SERVER' + (this.options.instanceId ? ' ' + 346 | this.options.instanceId : '') + ':\n└──' + str.replace(/\r?\n/g, '\n ')); 347 | } 348 | if (this.options.logFile) { 349 | this.log('SERVER' + (this.options.instanceId ? ' ' + 350 | this.options.instanceId : '') + ':\n└──' + str.replace(/\r?\n/g, '\n ')); 351 | } 352 | 353 | if (typeof this._currentAction == 'function') { 354 | this._currentAction.call(this, str); 355 | } 356 | }; 357 | 358 | /** 359 | *

'error' listener for the socket

360 | * 361 | * @event 362 | * @param {Error} err Error object 363 | * @param {String} type Error name 364 | */ 365 | SMTPClient.prototype._onError = function(err, type, data) { 366 | if (type && type != 'Error') { 367 | err.name = type; 368 | } 369 | if (data) { 370 | err.data = data; 371 | } 372 | err.stage = this.stage; 373 | this.emit('error', err); 374 | this.close(); 375 | }; 376 | 377 | /** 378 | *

'drain' listener for the socket

379 | * 380 | * @event 381 | */ 382 | SMTPClient.prototype._onDrain = function() { 383 | this.emit('drain'); 384 | }; 385 | 386 | 387 | /** 388 | *

'close' listener for the socket

389 | * 390 | * @event 391 | */ 392 | SMTPClient.prototype._onClose = function() { 393 | if ([this._actionGreeting, this._actionIdle, this.close].indexOf(this._currentAction) < 0 && !this._destroyed) { 394 | return this._onError(new Error('Connection closed unexpectedly')); 395 | } 396 | 397 | this.stage = 'close'; 398 | 399 | this._destroy(); 400 | }; 401 | 402 | /** 403 | *

'end' listener for the socket

404 | * 405 | * @event 406 | */ 407 | SMTPClient.prototype._onEnd = function() { 408 | this.stage = 'end'; 409 | 410 | this._destroy(); 411 | }; 412 | 413 | /** 414 | *

'timeout' listener for the socket

415 | * 416 | * @event 417 | */ 418 | SMTPClient.prototype._onTimeout = function() { 419 | this.close(); 420 | }; 421 | 422 | /** 423 | *

Passes data stream to socket if in data mode

424 | * 425 | * @param {Buffer} chunk Chunk of data to be sent to the server 426 | */ 427 | SMTPClient.prototype.write = function(chunk) { 428 | // works only in data mode 429 | if (!this._dataMode || this._destroyed) { 430 | // this line should never be reached but if it does, then 431 | // say act like everything's normal. 432 | return true; 433 | } 434 | 435 | if (typeof chunk == 'string') { 436 | chunk = new Buffer(chunk, 'utf-8'); 437 | } 438 | 439 | if (!this.options.enableDotEscaping) { 440 | if (chunk.length >= 2) { 441 | this._lastDataBytes[0] = chunk[chunk.length - 2]; 442 | this._lastDataBytes[1] = chunk[chunk.length - 1]; 443 | } else if (chunk.length == 1) { 444 | this._lastDataBytes[0] = this._lastDataBytes[1]; 445 | this._lastDataBytes[1] = chunk[0]; 446 | } 447 | } else { 448 | chunk = this._escapeDot(chunk); 449 | } 450 | 451 | if (this.options.debug) { 452 | console.log('CLIENT (DATA)' + (this.options.instanceId ? ' ' + 453 | this.options.instanceId : '') + ':\n└──' + chunk.toString().trim().replace(/\n/g, '\n ')); 454 | } 455 | if (this.options.logFile) { 456 | this.log('CLIENT (DATA)' + (this.options.instanceId ? ' ' + 457 | this.options.instanceId : '') + ':\n└──' + chunk.toString().trim().replace(/\n/g, '\n ')); 458 | } 459 | 460 | // pass the chunk to the socket 461 | return this.socket.write(chunk); 462 | }; 463 | 464 | /** 465 | *

Indicates that a data stream for the socket is ended. Works only 466 | * in data mode.

467 | * 468 | * @param {Buffer} [chunk] Chunk of data to be sent to the server 469 | */ 470 | SMTPClient.prototype.end = function(chunk) { 471 | // works only in data mode 472 | if (!this._dataMode || this._destroyed) { 473 | // this line should never be reached but if it does, then 474 | // say act like everything's normal. 475 | return true; 476 | } 477 | 478 | if (chunk && chunk.length) { 479 | this.write(chunk); 480 | } 481 | 482 | // redirect output from the server to _actionStream 483 | this._currentAction = this._actionStream; 484 | 485 | // indicate that the stream has ended by sending a single dot on its own line 486 | // if the client already closed the data with \r\n no need to do it again 487 | if (this._lastDataBytes[0] == 0x0D && this._lastDataBytes[1] == 0x0A) { 488 | this.socket.write(new Buffer('.\r\n', 'utf-8')); 489 | } else if (this._lastDataBytes[1] == 0x0D) { 490 | this.socket.write(new Buffer('\n.\r\n')); 491 | } else { 492 | this.socket.write(new Buffer('\r\n.\r\n')); 493 | } 494 | this._lastDataBytes[0] = 0x0D; 495 | this._lastDataBytes[1] = 0x0A; 496 | 497 | 498 | // end data mode 499 | this._dataMode = false; 500 | }; 501 | 502 | /** 503 | *

Send a command to the server, append \r\n

504 | * 505 | * @param {String} str String to be sent to the server 506 | */ 507 | SMTPClient.prototype.sendCommand = function(str) { 508 | if (this._destroyed) { 509 | // Connection already closed, can't send any more data 510 | return; 511 | } 512 | if (this.socket.destroyed) { 513 | return this.close(); 514 | } 515 | if (this.options.debug) { 516 | console.log('CLIENT' + (this.options.instanceId ? ' ' + 517 | this.options.instanceId : '') + ':\n└──' + (str || '').toString().trim().replace(/\n/g, '\n ')); 518 | } 519 | if (this.options.logFile) { 520 | this.log('CLIENT' + (this.options.instanceId ? ' ' + 521 | this.options.instanceId : '') + ':\n└──' + (str || '').toString().trim().replace(/\n/g, '\n ')); 522 | } 523 | this.socket.write(new Buffer(str + '\r\n', 'utf-8')); 524 | }; 525 | 526 | /** 527 | *

Sends QUIT

528 | */ 529 | SMTPClient.prototype.quit = function() { 530 | this._closing = true; 531 | this.sendCommand('QUIT'); 532 | this._currentAction = this.close; 533 | }; 534 | 535 | /** 536 | *

Closes the connection to the server

537 | */ 538 | SMTPClient.prototype.close = function() { 539 | this._closing = true; 540 | 541 | if (this.options.debug) { 542 | console.log('Closing connection to the server'); 543 | } 544 | 545 | if (this.options.logFile) { 546 | this.log('Closing connection to the server'); 547 | } 548 | 549 | var closeMethod = 'end'; 550 | 551 | // Clear current job 552 | this._currentAction = this._actionIdle; 553 | 554 | if (this.stage === 'init') { 555 | // Clear connection timeout timer if other than timeout error occurred 556 | clearTimeout(this._connectionTimeout); 557 | // Close the socket immediately when connection timed out 558 | closeMethod = 'destroy'; 559 | } 560 | 561 | if (this.socket && this.socket.socket && this.socket.socket[closeMethod] && !this.socket.socket.destroyed) { 562 | this.socket.socket[closeMethod](); 563 | } 564 | if (this.socket && this.socket[closeMethod] && !this.socket.destroyed) { 565 | this.socket[closeMethod](); 566 | } 567 | this._destroy(); 568 | }; 569 | 570 | /** 571 | *

Initiates a new message by submitting envelope data, starting with 572 | * MAIL FROM: command

573 | * 574 | * @param {Object} envelope Envelope object in the form of 575 | * {from:'...', to:['...']} 576 | * or 577 | * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']} 578 | */ 579 | SMTPClient.prototype.useEnvelope = function(envelope) { 580 | this._envelope = envelope || {}; 581 | this._envelope.from = this._envelope.from && this._envelope.from.address || this._envelope.from || ('anonymous@' + this.options.name); 582 | 583 | this._envelope.to = [].concat(this._envelope.to || []).map(function(to) { 584 | return to && to.address || to; 585 | }); 586 | 587 | // clone the recipients array for latter manipulation 588 | this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || [])); 589 | this._envelope.rcptFailed = []; 590 | 591 | this._currentAction = this._actionMAIL; 592 | this.sendCommand('MAIL FROM:<' + (this._envelope.from) + '>'); 593 | }; 594 | 595 | /** 596 | *

If needed starts the authentication, if not emits 'idle' to 597 | * indicate that this client is ready to take in an outgoing mail

598 | */ 599 | SMTPClient.prototype._authenticateUser = function() { 600 | this.stage = 'auth'; 601 | 602 | if (!this.options.auth) { 603 | // no need to authenticate, at least no data given 604 | this._enterIdle(); 605 | return; 606 | } 607 | 608 | var auth; 609 | if (this.options.auth.XOAuthToken && this._supportedAuth.indexOf('XOAUTH') >= 0) { 610 | auth = 'XOAUTH'; 611 | } else if (this._xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) { 612 | auth = 'XOAUTH2'; 613 | } else if (this.options.authMethod) { 614 | auth = this.options.authMethod.toUpperCase().trim(); 615 | } else { 616 | // use first supported 617 | auth = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); 618 | } 619 | 620 | switch (auth) { 621 | case 'XOAUTH': 622 | this._currentAction = this._actionAUTHComplete; 623 | 624 | if (typeof this.options.auth.XOAuthToken == 'object' && 625 | typeof this.options.auth.XOAuthToken.generate == 'function') { 626 | this.options.auth.XOAuthToken.generate((function(err, XOAuthToken) { 627 | if (this._destroyed) { 628 | // Nothing to do here anymore, connection already closed 629 | return; 630 | } 631 | if (err) { 632 | return this._onError(err, 'XOAuthTokenError'); 633 | } 634 | this.sendCommand('AUTH XOAUTH ' + XOAuthToken); 635 | }).bind(this)); 636 | } else { 637 | this.sendCommand('AUTH XOAUTH ' + this.options.auth.XOAuthToken.toString()); 638 | } 639 | return; 640 | case 'XOAUTH2': 641 | this._currentAction = this._actionAUTHComplete; 642 | this._xoauth2.getToken((function(err, token) { 643 | if (this._destroyed) { 644 | // Nothing to do here anymore, connection already closed 645 | return; 646 | } 647 | if (err) { 648 | this._onError(err, 'XOAUTH2Error'); 649 | return; 650 | } 651 | this.sendCommand('AUTH XOAUTH2 ' + token); 652 | }).bind(this)); 653 | return; 654 | case 'LOGIN': 655 | this._currentAction = this._actionAUTH_LOGIN_USER; 656 | this.sendCommand('AUTH LOGIN'); 657 | return; 658 | case 'PLAIN': 659 | this._currentAction = this._actionAUTHComplete; 660 | this.sendCommand('AUTH PLAIN ' + new Buffer( 661 | //this.options.auth.user+'\u0000'+ 662 | '\u0000' + // skip authorization identity as it causes problems with some servers 663 | this.options.auth.user + '\u0000' + 664 | this.options.auth.pass, 'utf-8').toString('base64')); 665 | return; 666 | case 'CRAM-MD5': 667 | this._currentAction = this._actionAUTH_CRAM_MD5; 668 | this.sendCommand('AUTH CRAM-MD5'); 669 | return; 670 | } 671 | 672 | this._onError(new Error('Unknown authentication method - ' + auth), 'UnknowAuthError'); 673 | }; 674 | 675 | /** ACTIONS **/ 676 | 677 | /** 678 | *

Will be run after the connection is created and the server sends 679 | * a greeting. If the incoming message starts with 220 initiate 680 | * SMTP session by sending EHLO command

681 | * 682 | * @param {String} str Message from the server 683 | */ 684 | SMTPClient.prototype._actionGreeting = function(str) { 685 | this.stage = 'greeting'; 686 | 687 | clearTimeout(this._greetingTimeout); 688 | 689 | if (str.substr(0, 3) != '220') { 690 | this._onError(new Error('Invalid greeting from server - ' + str), false, str); 691 | return; 692 | } 693 | 694 | this._currentAction = this._actionEHLO; 695 | this.sendCommand('EHLO ' + this.options.name); 696 | }; 697 | 698 | /** 699 | *

Handles server response for EHLO command. If it yielded in 700 | * error, try HELO instead, otherwise initiate TLS negotiation 701 | * if STARTTLS is supported by the server or move into the 702 | * authentication phase.

703 | * 704 | * @param {String} str Message from the server 705 | */ 706 | SMTPClient.prototype._actionEHLO = function(str) { 707 | this.stage = 'ehlo'; 708 | 709 | if (str.substr(0, 3) == '421') { 710 | this._onError(new Error('Server terminates connection - ' + str), false, str); 711 | return; 712 | } 713 | 714 | if (str.charAt(0) != '2') { 715 | // Try HELO instead 716 | this._currentAction = this._actionHELO; 717 | this.sendCommand('HELO ' + this.options.name); 718 | return; 719 | } 720 | 721 | // Detect if the server supports STARTTLS 722 | if (!this._secureMode && str.match(/[ \-]STARTTLS\r?$/mi)) { 723 | this.sendCommand('STARTTLS'); 724 | this._currentAction = this._actionSTARTTLS; 725 | return; 726 | } 727 | 728 | // Detect if the server supports PLAIN auth 729 | if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i)) { 730 | this._supportedAuth.push('PLAIN'); 731 | } 732 | 733 | // Detect if the server supports LOGIN auth 734 | if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i)) { 735 | this._supportedAuth.push('LOGIN'); 736 | } 737 | 738 | // Detect if the server supports CRAM-MD5 auth 739 | if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i)) { 740 | this._supportedAuth.push('CRAM-MD5'); 741 | } 742 | 743 | // Detect if the server supports XOAUTH auth 744 | if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH/i)) { 745 | this._supportedAuth.push('XOAUTH'); 746 | } 747 | 748 | // Detect if the server supports XOAUTH2 auth 749 | if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i)) { 750 | this._supportedAuth.push('XOAUTH2'); 751 | } 752 | 753 | this._authenticateUser.call(this); 754 | }; 755 | 756 | /** 757 | *

Handles server response for HELO command. If it yielded in 758 | * error, emit 'error', otherwise move into the authentication phase.

759 | * 760 | * @param {String} str Message from the server 761 | */ 762 | SMTPClient.prototype._actionHELO = function(str) { 763 | this.stage = 'helo'; 764 | 765 | if (str.charAt(0) != '2') { 766 | this._onError(new Error('Invalid response for EHLO/HELO - ' + str), false, str); 767 | return; 768 | } 769 | this._authenticateUser.call(this); 770 | }; 771 | 772 | /** 773 | *

Handles server response for STARTTLS command. If there's an error 774 | * try HELO instead, otherwise initiate TLS upgrade. If the upgrade 775 | * succeedes restart the EHLO

776 | * 777 | * @param {String} str Message from the server 778 | */ 779 | SMTPClient.prototype._actionSTARTTLS = function(str) { 780 | this.stage = 'starttls'; 781 | 782 | if (str.charAt(0) != '2') { 783 | // Try HELO instead 784 | this._currentAction = this._actionHELO; 785 | this.sendCommand('HELO ' + this.options.name); 786 | return; 787 | } 788 | 789 | this._upgradeConnection((function(err, secured) { 790 | if (err) { 791 | this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'TLSError'); 792 | return; 793 | } 794 | if (this.options.debug) { 795 | console.log('Connection secured'); 796 | } 797 | if (this.options.logFile) { 798 | this.log('Connection secured'); 799 | } 800 | 801 | if (secured) { 802 | // restart session 803 | this._currentAction = this._actionEHLO; 804 | this.sendCommand('EHLO ' + this.options.name); 805 | } else { 806 | this._authenticateUser.call(this); 807 | } 808 | }).bind(this)); 809 | }; 810 | 811 | /** 812 | *

Handle the response for AUTH LOGIN command. We are expecting 813 | * '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as 814 | * response needs to be base64 encoded username.

815 | * 816 | * @param {String} str Message from the server 817 | */ 818 | SMTPClient.prototype._actionAUTH_LOGIN_USER = function(str) { 819 | if (str != '334 VXNlcm5hbWU6') { 820 | this._onError(new Error('Invalid login sequence while waiting for "334 VXNlcm5hbWU6" - ' + str), false, str); 821 | return; 822 | } 823 | this._currentAction = this._actionAUTH_LOGIN_PASS; 824 | this.sendCommand(new Buffer( 825 | this.options.auth.user + '', 'utf-8').toString('base64')); 826 | }; 827 | 828 | /** 829 | *

Handle the response for AUTH CRAM-MD5 command. We are expecting 830 | * '334 '. Data to be sent as response needs to be 831 | * base64 decoded challenge string, MD5 hashed using the password as 832 | * a HMAC key, prefixed by the username and a space, and finally all 833 | * base64 encoded again.

834 | * 835 | * @param {String} str Message from the server 836 | */ 837 | SMTPClient.prototype._actionAUTH_CRAM_MD5 = function(str) { 838 | var challengeMatch = str.match(/^334\s+(.+)$/), 839 | challengeString = ''; 840 | 841 | if (!challengeMatch) { 842 | this._onError(new Error('Invalid login sequence while waiting for server challenge string - ' + str), false, str); 843 | return; 844 | } else { 845 | challengeString = challengeMatch[1]; 846 | } 847 | 848 | // Decode from base64 849 | var base64decoded = new Buffer(challengeString, 'base64').toString('ascii'), 850 | hmac_md5 = crypto.createHmac('md5', this.options.auth.pass); 851 | hmac_md5.update(base64decoded); 852 | var hex_hmac = hmac_md5.digest('hex'), 853 | prepended = this.options.auth.user + ' ' + hex_hmac; 854 | 855 | this._currentAction = this._actionAUTH_CRAM_MD5_PASS; 856 | 857 | this.sendCommand(new Buffer(prepended).toString('base64')); 858 | }; 859 | 860 | /** 861 | *

Handles the response to CRAM-MD5 authentication, if there's no error, 862 | * the user can be considered logged in. Emit 'idle' and start 863 | * waiting for a message to send

864 | * 865 | * @param {String} str Message from the server 866 | */ 867 | SMTPClient.prototype._actionAUTH_CRAM_MD5_PASS = function(str) { 868 | if (!str.match(/^235\s+/)) { 869 | this._onError(new Error('Invalid login sequence while waiting for "235 go ahead" - ' + str), false, str); 870 | return; 871 | } 872 | this._enterIdle(); 873 | }; 874 | 875 | /** 876 | *

Handle the response for AUTH LOGIN command. We are expecting 877 | * '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as 878 | * response needs to be base64 encoded password.

879 | * 880 | * @param {String} str Message from the server 881 | */ 882 | SMTPClient.prototype._actionAUTH_LOGIN_PASS = function(str) { 883 | if (str != '334 UGFzc3dvcmQ6') { 884 | this._onError(new Error('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6" - ' + str), false, str); 885 | return; 886 | } 887 | this._currentAction = this._actionAUTHComplete; 888 | this.sendCommand(new Buffer(this.options.auth.pass + '', 'utf-8').toString('base64')); 889 | }; 890 | 891 | /** 892 | *

Handles the response for authentication, if there's no error, 893 | * the user can be considered logged in. Emit 'idle' and start 894 | * waiting for a message to send

895 | * 896 | * @param {String} str Message from the server 897 | */ 898 | SMTPClient.prototype._actionAUTHComplete = function(str) { 899 | var response; 900 | 901 | if (this._xoauth2 && str.substr(0, 3) == '334') { 902 | try { 903 | response = str.split(' '); 904 | response.shift(); 905 | response = JSON.parse(new Buffer(response.join(' '), 'base64').toString('utf-8')); 906 | 907 | if ((!this._xoauth2.reconnectCount || this._xoauth2.reconnectCount < 200) && ['400', '401'].indexOf(response.status) >= 0) { 908 | this._xoauth2.reconnectCount = (this._xoauth2.reconnectCount || 0) + 1; 909 | this._currentAction = this._actionXOAUTHRetry; 910 | } else { 911 | this._xoauth2.reconnectCount = 0; 912 | this._currentAction = this._actionAUTHComplete; 913 | } 914 | this.sendCommand(new Buffer(0)); 915 | return; 916 | 917 | } catch (E) {} 918 | } 919 | 920 | if(this._xoauth2){ 921 | this._xoauth2.reconnectCount = 0; 922 | } 923 | 924 | if (str.charAt(0) != '2') { 925 | this._onError(new Error('Invalid login - ' + str), 'AuthError', str); 926 | return; 927 | } 928 | 929 | this._enterIdle(); 930 | }; 931 | 932 | /** 933 | * If XOAUTH2 authentication failed, try again by generating 934 | * new access token 935 | */ 936 | SMTPClient.prototype._actionXOAUTHRetry = function() { 937 | 938 | // ensure that something is listening unexpected responses 939 | this._currentAction = this._actionIdle; 940 | 941 | this._xoauth2.generateToken((function(err, token) { 942 | if (this._destroyed) { 943 | // Nothing to do here anymore, connection already closed 944 | return; 945 | } 946 | if (err) { 947 | this._onError(err, 'XOAUTH2Error'); 948 | return; 949 | } 950 | this._currentAction = this._actionAUTHComplete; 951 | this.sendCommand('AUTH XOAUTH2 ' + token); 952 | }).bind(this)); 953 | }; 954 | 955 | /** 956 | *

This function is not expected to run. If it does then there's probably 957 | * an error (timeout etc.)

958 | * 959 | * @param {String} str Message from the server 960 | */ 961 | SMTPClient.prototype._actionIdle = function(str) { 962 | this.stage = 'idle'; 963 | 964 | if (Number(str.charAt(0)) > 3) { 965 | this._onError(new Error(str), false, str); 966 | return; 967 | } 968 | 969 | // this line should never get called 970 | }; 971 | 972 | /** 973 | *

Handle response for a MAIL FROM: command

974 | * 975 | * @param {String} str Message from the server 976 | */ 977 | SMTPClient.prototype._actionMAIL = function(str) { 978 | this.stage = 'mail'; 979 | 980 | if (Number(str.charAt(0)) != '2') { 981 | this._onError(new Error('Mail from command failed - ' + str), 'SenderError', str); 982 | return; 983 | } 984 | 985 | if (!this._envelope.rcptQueue.length) { 986 | this._onError(new Error('Can\'t send mail - no recipients defined'), 'RecipientError', str); 987 | } else { 988 | this._envelope.curRecipient = this._envelope.rcptQueue.shift(); 989 | this._currentAction = this._actionRCPT; 990 | this.sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>' + this._getDSN()); 991 | } 992 | }; 993 | 994 | /** 995 | * Emits 'idle' 996 | */ 997 | SMTPClient.prototype._enterIdle = function() { 998 | this._currentAction = this._actionIdle; 999 | this.emit('idle'); // ready to take orders 1000 | }; 1001 | 1002 | /** 1003 | *

SetsUp DSN

1004 | */ 1005 | SMTPClient.prototype._getDSN = function() { 1006 | var ret = '', 1007 | n = [], 1008 | dsn; 1009 | 1010 | if (this.currentMessage && this.currentMessage.options && 'dsn' in this.currentMessage.options) { 1011 | dsn = this.currentMessage.options.dsn; 1012 | 1013 | if (dsn.success) { 1014 | n.push('SUCCESS'); 1015 | } 1016 | 1017 | if (dsn.failure) { 1018 | n.push('FAILURE'); 1019 | } 1020 | 1021 | if (dsn.delay) { 1022 | n.push('DELAY'); 1023 | } 1024 | 1025 | if (n.length > 0) { 1026 | ret = ' NOTIFY=' + n.join(',') + ' ORCPT=rfc822;' + this.currentMessage._message.from; 1027 | } 1028 | } 1029 | 1030 | return ret; 1031 | }; 1032 | 1033 | /** 1034 | *

Handle response for a RCPT TO: command

1035 | * 1036 | * @param {String} str Message from the server 1037 | */ 1038 | SMTPClient.prototype._actionRCPT = function(str) { 1039 | this.stage = 'rcpt'; 1040 | 1041 | if (Number(str.charAt(0)) != '2') { 1042 | // this is a soft error 1043 | this._envelope.rcptFailed.push(this._envelope.curRecipient); 1044 | } 1045 | 1046 | if (!this._envelope.rcptQueue.length) { 1047 | if (this._envelope.rcptFailed.length < this._envelope.to.length) { 1048 | if (this._envelope.rcptFailed.length) { 1049 | this.emit('rcptFailed', this._envelope.rcptFailed); 1050 | } 1051 | this._currentAction = this._actionDATA; 1052 | this.sendCommand('DATA'); 1053 | } else { 1054 | this._onError(new Error('Can\'t send mail - all recipients were rejected'), 'RecipientError', str); 1055 | return; 1056 | } 1057 | } else { 1058 | this._envelope.curRecipient = this._envelope.rcptQueue.shift(); 1059 | this._currentAction = this._actionRCPT; 1060 | this.sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>'); 1061 | } 1062 | }; 1063 | 1064 | /** 1065 | *

Handle response for a DATA command

1066 | * 1067 | * @param {String} str Message from the server 1068 | */ 1069 | SMTPClient.prototype._actionDATA = function(str) { 1070 | this.stage = 'data'; 1071 | 1072 | // response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24 1073 | // some servers might use 250 instead, so lets check for 2 or 3 as the first digit 1074 | if ([2, 3].indexOf(Number(str.charAt(0))) < 0) { 1075 | this._onError(new Error('Data command failed - ' + str), false, str); 1076 | return; 1077 | } 1078 | 1079 | // Emit that connection is set up for streaming 1080 | this._dataMode = true; 1081 | this._currentAction = this._actionIdle; 1082 | this.emit('message'); 1083 | }; 1084 | 1085 | /** 1086 | *

Handle response for a DATA stream

1087 | * 1088 | * @param {String} str Message from the server 1089 | */ 1090 | SMTPClient.prototype._actionStream = function(str) { 1091 | if (Number(str.charAt(0)) != '2') { 1092 | // Message failed 1093 | this.emit('ready', false, str); 1094 | } else { 1095 | // Message sent succesfully 1096 | this.emit('ready', true, str); 1097 | } 1098 | 1099 | // Waiting for new connections 1100 | this._currentAction = this._actionIdle; 1101 | 1102 | if (typeof setImmediate == 'function') { 1103 | setImmediate(this._enterIdle.bind(this)); 1104 | } else { 1105 | process.nextTick(this._enterIdle.bind(this)); 1106 | } 1107 | }; 1108 | 1109 | /** 1110 | *

Log debugs to given file

1111 | * 1112 | * @param {String} str Log message 1113 | */ 1114 | SMTPClient.prototype.log = function(str) { 1115 | fs.appendFile(this.options.logFile, str + '\n', function(err) { 1116 | if (err) { 1117 | console.log('Log write failed. Data to log: ' + str); 1118 | } 1119 | }); 1120 | }; 1121 | 1122 | /** 1123 | *

Inserts an extra dot at the begining of a line if it starts with a dot 1124 | * See RFC 2821 Section 4.5.2

1125 | * 1126 | * @param {Buffer} chunk The chunk that will be send. 1127 | */ 1128 | SMTPClient.prototype._escapeDot = function(chunk) { 1129 | var pos, OutBuff, i; 1130 | OutBuff = new Buffer(chunk.length * 2); 1131 | pos = 0; 1132 | 1133 | for (i = 0; i < chunk.length; i++) { 1134 | if (this._lastDataBytes[0] == 0x0D && this._lastDataBytes[1] == 0x0A && chunk[i] == 0x2E) { 1135 | OutBuff[pos] = 0x2E; 1136 | pos += 1; 1137 | } 1138 | OutBuff[pos] = chunk[i]; 1139 | pos += 1; 1140 | this._lastDataBytes[0] = this._lastDataBytes[1]; 1141 | this._lastDataBytes[1] = chunk[i]; 1142 | } 1143 | 1144 | return OutBuff.slice(0, pos); 1145 | }; -------------------------------------------------------------------------------- /lib/pool.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var simplesmtp = require('../index'), 4 | EventEmitter = require('events').EventEmitter, 5 | utillib = require('util'), 6 | xoauth2 = require('xoauth2'); 7 | 8 | // expose to the world 9 | module.exports = function(port, host, options) { 10 | var pool = new SMTPConnectionPool(port, host, options); 11 | return pool; 12 | }; 13 | 14 | /** 15 | *

Creates a SMTP connection pool

16 | * 17 | *

Optional options object takes the following possible properties:

18 | * 27 | * 28 | * @constructor 29 | * @namespace SMTP Client Pool module 30 | * @param {Number} [port=25] The port number to connecto to 31 | * @param {String} [host='localhost'] THe hostname to connect to 32 | * @param {Object} [options] optional options object 33 | */ 34 | function SMTPConnectionPool(port, host, options) { 35 | EventEmitter.call(this); 36 | 37 | /** 38 | * Port number to connect to 39 | * @public 40 | */ 41 | this.port = port || 25; 42 | 43 | /** 44 | * Hostname to connect to 45 | * @public 46 | */ 47 | this.host = host || 'localhost'; 48 | 49 | /** 50 | * Options object 51 | * @public 52 | */ 53 | this.options = options || {}; 54 | this.options.maxConnections = this.options.maxConnections || 5; 55 | this.options.maxMessages = this.options.maxMessages || Infinity; 56 | 57 | /** 58 | * An array of connections that are currently idle 59 | * @private 60 | */ 61 | this._connectionsAvailable = []; 62 | 63 | /** 64 | * An array of connections that are currently in use 65 | * @private 66 | */ 67 | this._connectionsInUse = []; 68 | 69 | /** 70 | * Message queue (FIFO) 71 | * @private 72 | */ 73 | this._messageQueue = []; 74 | 75 | /** 76 | * Counter for generating ID values for debugging 77 | * @private 78 | */ 79 | this._idgen = 0; 80 | 81 | // Initialize XOAUTH2 if needed 82 | if (this.options.auth && typeof this.options.auth.XOAuth2 == 'object') { 83 | if (!this.options.auth.XOAuth2.user && this.options.auth.user) { 84 | this.options.auth.XOAuth2.user = this.options.auth.user; 85 | } 86 | this.options.auth.XOAuth2 = xoauth2.createXOAuth2Generator(this.options.auth.XOAuth2); 87 | } 88 | } 89 | utillib.inherits(SMTPConnectionPool, EventEmitter); 90 | 91 | /** 92 | *

Sends a message. If there's any idling connections available 93 | * use one to send the message immediatelly, otherwise add to queue.

94 | * 95 | * @param {Object} message MailComposer object 96 | * @param {Function} callback Callback function to run on finish, gets an 97 | * error object as a parameter if the sending failed 98 | * and on success an object with failedRecipients array as 99 | * a list of addresses that were rejected (if any) and 100 | * message which indicates the last message received from 101 | * the server 102 | */ 103 | SMTPConnectionPool.prototype.sendMail = function(message, callback) { 104 | var connection; 105 | 106 | message.returnCallback = callback; 107 | 108 | if (this._connectionsAvailable.length) { 109 | // if available connections pick one 110 | connection = this._connectionsAvailable.pop(); 111 | this._connectionsInUse.push(connection); 112 | this._processMessage(message, connection); 113 | } else { 114 | this._messageQueue.push(message); 115 | if (this._connectionsAvailable.length + this._connectionsInUse.length < this.options.maxConnections) { 116 | this._createConnection(); 117 | } 118 | } 119 | }; 120 | 121 | /** 122 | *

Closes all connections

123 | */ 124 | SMTPConnectionPool.prototype.close = function(callback) { 125 | var connection; 126 | 127 | // for some reason destroying the connections seem to be the only way :S 128 | while (this._connectionsAvailable.length) { 129 | connection = this._connectionsAvailable.pop(); 130 | connection.quit(); 131 | } 132 | 133 | while (this._connectionsInUse.length) { 134 | connection = this._connectionsInUse.pop(); 135 | connection.quit(); 136 | } 137 | 138 | if (callback) { 139 | if (typeof setImmediate == 'function') { 140 | setImmediate(callback); 141 | } else { 142 | process.nextTick(callback); 143 | } 144 | } 145 | }; 146 | 147 | /** 148 | *

Initiates a connection to the SMTP server and adds it to the pool

149 | */ 150 | SMTPConnectionPool.prototype._createConnection = function() { 151 | 152 | var connectionOptions = { 153 | instanceId: ++this._idgen, 154 | debug: !! this.options.debug, 155 | logFile: this.options.logFile, 156 | ignoreTLS: !! this.options.ignoreTLS, 157 | tls: this.options.tls || false, 158 | auth: this.options.auth || false, 159 | authMethod: this.options.authMethod, 160 | name: this.options.name || false, 161 | secureConnection: !! this.options.secureConnection 162 | }, 163 | connection; 164 | 165 | if ('greetingTimeout' in this.options) { 166 | connectionOptions.greetingTimeout = this.options.greetingTimeout; 167 | } 168 | 169 | if ('socketTimeout' in this.options) { 170 | connectionOptions.socketTimeout = this.options.socketTimeout; 171 | } 172 | 173 | if ('connectionTimeout' in this.options) { 174 | connectionOptions.connectionTimeout = this.options.connectionTimeout; 175 | } 176 | 177 | if ('rejectUnathorized' in this.options) { 178 | connectionOptions.rejectUnathorized = this.options.rejectUnathorized; 179 | } 180 | 181 | if ('localAddress' in this.options) { 182 | connectionOptions.localAddress = this.options.localAddress; 183 | } 184 | 185 | connection = simplesmtp.connect(this.port, this.host, connectionOptions); 186 | 187 | connection._messagesProcessed = 0; 188 | 189 | connection.on('idle', this._onConnectionIdle.bind(this, connection)); 190 | connection.on('message', this._onConnectionMessage.bind(this, connection)); 191 | connection.on('ready', this._onConnectionReady.bind(this, connection)); 192 | connection.on('error', this._onConnectionError.bind(this, connection)); 193 | connection.on('end', this._onConnectionEnd.bind(this, connection)); 194 | connection.on('rcptFailed', this._onConnectionRCPTFailed.bind(this, connection)); 195 | 196 | this.emit('connectionCreated', connection); 197 | 198 | // as the connection is not ready yet, add to 'in use' queue 199 | this._connectionsInUse.push(connection); 200 | }; 201 | 202 | /** 203 | *

Processes a message by assigning it to a connection object and initiating 204 | * the sending process by setting the envelope

205 | * 206 | * @param {Object} message MailComposer message object 207 | * @param {Object} connection simplesmtp.connect connection 208 | */ 209 | SMTPConnectionPool.prototype._processMessage = function(message, connection) { 210 | connection.currentMessage = message; 211 | message.currentConnection = connection; 212 | 213 | connection._messagesProcessed++; 214 | 215 | // send envelope 216 | connection.useEnvelope(message.getEnvelope()); 217 | }; 218 | 219 | /** 220 | *

Will be fired on 'idle' events by the connection, if 221 | * there's a message currently in queue

222 | * 223 | * @event 224 | * @param {Object} connection Connection object that fired the event 225 | */ 226 | SMTPConnectionPool.prototype._onConnectionIdle = function(connection) { 227 | var message = this._messageQueue.shift(); 228 | 229 | if (message) { 230 | this._processMessage(message, connection); 231 | } else { 232 | for (var i = 0, len = this._connectionsInUse.length; i < len; i++) { 233 | if (this._connectionsInUse[i] == connection) { 234 | this._connectionsInUse.splice(i, 1); // remove from list 235 | break; 236 | } 237 | } 238 | this._connectionsAvailable.push(connection); 239 | } 240 | }; 241 | 242 | /** 243 | *

Will be called when not all recipients were accepted

244 | * 245 | * @event 246 | * @param {Object} connection Connection object that fired the event 247 | * @param {Array} addresses Failed addresses as an array of strings 248 | */ 249 | SMTPConnectionPool.prototype._onConnectionRCPTFailed = function(connection, addresses) { 250 | if (connection.currentMessage) { 251 | connection.currentMessage.failedRecipients = addresses; 252 | } 253 | }; 254 | 255 | /** 256 | *

Will be called when the client is waiting for a message to deliver

257 | * 258 | * @event 259 | * @param {Object} connection Connection object that fired the event 260 | */ 261 | SMTPConnectionPool.prototype._onConnectionMessage = function(connection) { 262 | if (connection.currentMessage) { 263 | connection.currentMessage.streamMessage(); 264 | connection.currentMessage.pipe(connection); 265 | } 266 | }; 267 | 268 | /** 269 | *

Will be called when a message has been delivered

270 | * 271 | * @event 272 | * @param {Object} connection Connection object that fired the event 273 | * @param {Boolean} success True if the message was queued by the SMTP server 274 | * @param {String} message Last message received from the server 275 | */ 276 | SMTPConnectionPool.prototype._onConnectionReady = function(connection, success, message) { 277 | var error, responseObj = {}; 278 | 279 | if (connection._messagesProcessed >= this.options.maxMessages && connection.socket) { 280 | 281 | connection.emit('end'); 282 | connection.removeAllListeners(); 283 | if (connection.socket) { 284 | connection.socket.destroy(); 285 | } 286 | 287 | this.emit('released', connection); 288 | } 289 | 290 | if (connection.currentMessage && connection.currentMessage.returnCallback) { 291 | if (success) { 292 | 293 | if (connection.currentMessage.failedRecipients) { 294 | responseObj.failedRecipients = connection.currentMessage.failedRecipients; 295 | } 296 | 297 | if (message) { 298 | responseObj.message = message; 299 | } 300 | 301 | if (connection.currentMessage._messageId) { 302 | responseObj.messageId = connection.currentMessage._messageId; 303 | } 304 | 305 | connection.currentMessage.returnCallback(null, responseObj); 306 | 307 | } else { 308 | error = new Error('Message delivery failed' + (message ? ': ' + message : '')); 309 | error.name = 'DeliveryError'; 310 | error.data = message; 311 | connection.currentMessage.returnCallback(error); 312 | } 313 | } 314 | connection.currentMessage = false; 315 | }; 316 | 317 | /** 318 | *

Will be called when an error occurs

319 | * 320 | * @event 321 | * @param {Object} connection Connection object that fired the event 322 | * @param {Object} error Error object 323 | */ 324 | SMTPConnectionPool.prototype._onConnectionError = function(connection, error) { 325 | var message = connection.currentMessage; 326 | connection.currentMessage = false; 327 | 328 | // clear a first message from the list, otherwise an infinite loop will emerge 329 | if (!message) { 330 | message = this._messageQueue.shift(); 331 | } 332 | 333 | if (message && message.returnCallback) { 334 | message.returnCallback(error); 335 | } 336 | }; 337 | 338 | /** 339 | *

Will be called when a connection to the client is closed

340 | * 341 | * @event 342 | * @param {Object} connection Connection object that fired the event 343 | */ 344 | SMTPConnectionPool.prototype._onConnectionEnd = function(connection) { 345 | var removed = false, 346 | i, len; 347 | 348 | // if in 'available' list, remove 349 | for (i = 0, len = this._connectionsAvailable.length; i < len; i++) { 350 | if (this._connectionsAvailable[i] == connection) { 351 | this._connectionsAvailable.splice(i, 1); // remove from list 352 | removed = true; 353 | break; 354 | } 355 | } 356 | 357 | if (!removed) { 358 | // if in 'in use' list, remove 359 | for (i = 0, len = this._connectionsInUse.length; i < len; i++) { 360 | if (this._connectionsInUse[i] == connection) { 361 | this._connectionsInUse.splice(i, 1); // remove from list 362 | removed = true; 363 | break; 364 | } 365 | } 366 | } 367 | 368 | // if there's still unprocessed mail and available connection slots, create 369 | // a new connection 370 | if (this._messageQueue.length && 371 | this._connectionsInUse.length + this._connectionsAvailable.length < this.options.maxConnections) { 372 | this._createConnection(); 373 | } 374 | }; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @fileOverview This is the main file for the simplesmtp library to create custom SMTP servers 5 | * @author Andris Reinman 6 | */ 7 | 8 | var RAIServer = require('rai').RAIServer, 9 | EventEmitter = require('events').EventEmitter, 10 | oslib = require('os'), 11 | utillib = require('util'), 12 | dnslib = require('dns'), 13 | crypto = require('crypto'); 14 | 15 | // expose to the world 16 | module.exports = function(options) { 17 | return new SMTPServer(options); 18 | }; 19 | 20 | /** 21 | *

Constructs a SMTP server

22 | * 23 | *

Possible options are:

24 | * 25 | * 43 | * 44 | * @constructor 45 | * @namespace SMTP Server module 46 | * @param {Object} [options] Options object 47 | */ 48 | function SMTPServer(options) { 49 | EventEmitter.call(this); 50 | 51 | this.connectedClients = 0; 52 | this.options = options || {}; 53 | this.options.name = this.options.name || (oslib.hostname && oslib.hostname()) || 54 | (oslib.getHostname && oslib.getHostname()) || 55 | '127.0.0.1'; 56 | 57 | this.options.authMethods = (this.options.authMethods || ['PLAIN', 'LOGIN']).map( 58 | function(auth) { 59 | return auth.toUpperCase().trim(); 60 | }); 61 | 62 | this.options.disableEHLO = !! this.options.disableEHLO; 63 | this.options.ignoreTLS = !! this.options.ignoreTLS; 64 | 65 | this.SMTPServer = new RAIServer({ 66 | secureConnection: !! this.options.secureConnection, 67 | credentials: this.options.credentials, 68 | timeout: this.options.timeout || 60 * 1000, 69 | disconnectOnTimeout: false, 70 | debug: !! this.options.debug 71 | }); 72 | 73 | this.SMTPServer.on('connect', this._createSMTPServerConnection.bind(this)); 74 | } 75 | utillib.inherits(SMTPServer, EventEmitter); 76 | 77 | /** 78 | * Server starts listening on defined port and hostname 79 | * 80 | * @param {Number} port The port number to listen 81 | * @param {String} [host] The hostname to listen 82 | * @param {Function} callback The callback function to run when the server is listening 83 | */ 84 | SMTPServer.prototype.listen = function(port, host, callback) { 85 | this.SMTPServer.listen(port, host, callback); 86 | }; 87 | 88 | /** 89 | *

Closes the server

90 | * 91 | * @param {Function} callback The callback function to run when the server is closed 92 | */ 93 | SMTPServer.prototype.end = function(callback) { 94 | this.SMTPServer.end(callback); 95 | }; 96 | 97 | /** 98 | *

Creates a new {@link SMTPServerConnection} object and links the main server with 99 | * the client socket

100 | * 101 | * @param {Object} client RAISocket object to a client 102 | */ 103 | SMTPServer.prototype._createSMTPServerConnection = function(client) { 104 | new SMTPServerConnection(this, client); 105 | }; 106 | 107 | /** 108 | *

Sets up a handler for the connected client

109 | * 110 | *

Restarts the state and sets up event listeners for client actions

111 | * 112 | * @constructor 113 | * @param {Object} server {@link SMTPServer} instance 114 | * @param {Object} client RAISocket instance for the client 115 | */ 116 | function SMTPServerConnection(server, client) { 117 | this.server = server; 118 | this.client = client; 119 | 120 | this.init(); 121 | this.server.connectedClients++; 122 | 123 | if (!this.client.remoteAddress) { 124 | if (this.server.options.debug) { 125 | console.log('Client already disconnected'); 126 | } 127 | this.client.end(); 128 | return; 129 | } 130 | 131 | if (this.server.options.debug) { 132 | console.log('Connection from', this.client.remoteAddress); 133 | } 134 | 135 | this.client.on('timeout', this._onTimeout.bind(this)); 136 | this.client.on('error', this._onError.bind(this)); 137 | this.client.on('command', this._onCommand.bind(this)); 138 | this.client.on('end', this._onEnd.bind(this)); 139 | 140 | this.client.on('data', this._onData.bind(this)); 141 | this.client.on('ready', this._onDataReady.bind(this)); 142 | 143 | // Too many clients. Disallow processing 144 | if (this.server.options.maxClients && this.server.connectedClients > this.server.options.maxClients) { 145 | this.end('421 ' + this.server.options.name + ' ESMTP - Too many connections. Please try again later.'); 146 | } else { 147 | // Send the greeting banner. Force ESMTP notice 148 | this.client.send('220 ' + this.server.options.name + ' ESMTP ' + (this.server.options.SMTPBanner || 'node.js simplesmtp')); 149 | } 150 | } 151 | 152 | /** 153 | *

Reset the envelope state

154 | * 155 | *

If keepAuthData is set to true, then doesn't remove 156 | * authentication data

157 | * 158 | * @param {Boolean} [keepAuthData=false] If set to true keep authentication data 159 | */ 160 | SMTPServerConnection.prototype.init = function(keepAuthData) { 161 | if (this.envelope === undefined) { 162 | this.envelope = {}; 163 | } 164 | 165 | this.envelope.from = ''; 166 | this.envelope.to = []; 167 | this.envelope.date = new Date(); 168 | 169 | if (this.hostNameAppearsAs) { 170 | this.envelope.host = this.hostNameAppearsAs; 171 | } 172 | 173 | if (this.client.remoteAddress) { 174 | this.envelope.remoteAddress = this.client.remoteAddress; 175 | } 176 | 177 | if (!keepAuthData) { 178 | this.authentication = { 179 | username: false, 180 | authenticated: false, 181 | state: 'NORMAL' 182 | }; 183 | } 184 | 185 | this.envelope.authentication = this.authentication; 186 | }; 187 | 188 | /** 189 | *

Sends a message to the client and closes the connection

190 | * 191 | * @param {String} [message] if set, send it to the client before disconnecting 192 | */ 193 | SMTPServerConnection.prototype.end = function(message) { 194 | if (message) { 195 | this.client.send(message); 196 | } 197 | this.client.end(); 198 | }; 199 | 200 | /** 201 | *

Will be called when the connection to the client is closed

202 | * 203 | * @event 204 | */ 205 | SMTPServerConnection.prototype._onEnd = function() { 206 | if (this.server.options.debug) { 207 | console.log('Connection closed to', this.client.remoteAddress); 208 | } 209 | this.server.connectedClients--; 210 | try { 211 | this.client.end(); 212 | } catch (E) {} 213 | this.server.emit('close', this.envelope); 214 | }; 215 | 216 | /** 217 | *

Will be called when timeout occurs

218 | * 219 | * @event 220 | */ 221 | SMTPServerConnection.prototype._onTimeout = function() { 222 | this.end('421 4.4.2 ' + this.server.options.name + ' Error: timeout exceeded'); 223 | }; 224 | 225 | /** 226 | *

Will be called when an error occurs

227 | * 228 | * @event 229 | */ 230 | SMTPServerConnection.prototype._onError = function() { 231 | this.end('421 4.4.2 ' + this.server.options.name + ' Error: client error'); 232 | }; 233 | 234 | /** 235 | *

Will be called when a command is received from the client

236 | * 237 | *

If there's curently an authentication process going on, route 238 | * the data to _handleAuthLogin, otherwise act as 239 | * defined

240 | * 241 | * @event 242 | * @param {String} command Command 243 | * @param {Buffer} command Payload related to the command 244 | */ 245 | SMTPServerConnection.prototype._onCommand = function(command, payload) { 246 | if (this.authentication.state == 'AUTHPLAINUSERDATA') { 247 | this._handleAuthPlain(command.toString('utf-8').trim().split(' ')); 248 | return; 249 | } 250 | 251 | if (this.authentication.state == 'AUTHENTICATING') { 252 | this._handleAuthLogin(command); 253 | return; 254 | } 255 | 256 | if (this.authentication.state == 'AUTHXOAUTH2') { 257 | this._handleAuthXOAuth2(command); 258 | return; 259 | } 260 | 261 | switch ((command || '').toString().trim().toUpperCase()) { 262 | 263 | // Should not occur too often 264 | case 'HELO': 265 | this._onCommandHELO(payload.toString('utf-8').trim()); 266 | break; 267 | 268 | // Lists server capabilities 269 | case 'EHLO': 270 | if (!this.server.options.disableEHLO) { 271 | this._onCommandEHLO(payload.toString('utf-8').trim()); 272 | } else { 273 | this.client.send('502 5.5.2 Error: command not recognized'); 274 | } 275 | break; 276 | 277 | // Closes the connection 278 | case 'QUIT': 279 | this.end('221 2.0.0 Goodbye!'); 280 | break; 281 | 282 | // Resets the current state 283 | case 'RSET': 284 | this._onCommandRSET(); 285 | break; 286 | 287 | // Doesn't work for spam related purposes 288 | case 'VRFY': 289 | this.client.send('252 2.1.5 Send some mail, I\'ll try my best'); 290 | break; 291 | 292 | // Initiate an e-mail by defining a sender 293 | case 'MAIL': 294 | this._onCommandMAIL(payload.toString('utf-8').trim()); 295 | break; 296 | 297 | // Add recipients to the e-mail envelope 298 | case 'RCPT': 299 | this._onCommandRCPT(payload.toString('utf-8').trim()); 300 | break; 301 | 302 | // Authenticate if needed 303 | case 'AUTH': 304 | this._onCommandAUTH(payload); 305 | break; 306 | 307 | // Start accepting binary data stream 308 | case 'DATA': 309 | this._onCommandDATA(); 310 | break; 311 | 312 | // Upgrade connection to secure TLS 313 | case 'STARTTLS': 314 | this._onCommandSTARTTLS(); 315 | break; 316 | 317 | // No operation 318 | case 'NOOP': 319 | this._onCommandNOOP(); 320 | break; 321 | 322 | // No operation 323 | case '': 324 | // ignore blank lines 325 | break; 326 | 327 | // Display an error on anything else 328 | default: 329 | this.client.send('502 5.5.2 Error: command not recognized'); 330 | } 331 | }; 332 | 333 | /** 334 | *

Initiate an e-mail by defining a sender.

335 | * 336 | *

This doesn't work if authorization is required but the client is 337 | * not logged in yet.

338 | * 339 | *

If validateSender option is set to true, then emits 340 | * 'validateSender' and wait for the callback before moving 341 | * on

342 | * 343 | * @param {String} mail Address payload in the form of 'FROM:<address>' 344 | */ 345 | SMTPServerConnection.prototype._onCommandMAIL = function(mail) { 346 | var self = this, 347 | match, 348 | email, 349 | domain; 350 | 351 | if (!this.hostNameAppearsAs) { 352 | return this.client.send('503 5.5.1 Error: send HELO/EHLO first'); 353 | } 354 | 355 | if (this.server.options.requireAuthentication && !this.authentication.authenticated) { 356 | return this.client.send('530 5.5.1 Authentication Required'); 357 | } 358 | 359 | if (this.envelope.from) { 360 | return this.client.send('503 5.5.1 Error: nested MAIL command'); 361 | } 362 | 363 | if (!(match = mail.match(/^from\:\s*<([^@>]+\@([^@>]+))>(\s|$)/i)) && !(mail.match(/^from\:\s*<>/i))) { 364 | return this.client.send('501 5.1.7 Bad sender address syntax'); 365 | } 366 | 367 | if (this.server.options.maxSize) { 368 | mail.replace(/> size=(\d+)\b\s*/i, function(o, size) { 369 | self.envelope.messageSize = size; 370 | }); 371 | } 372 | 373 | email = (match !== null && match[1]) || ''; 374 | domain = ((match !== null && match[2]) || '').toLowerCase(); 375 | 376 | this._validateAddress('sender', email, domain, function(err) { 377 | if (err) { 378 | return self.client.send(err.message); 379 | } 380 | email = email.substr(0, email.length - domain.length) + domain; 381 | self.envelope.from = email; 382 | self.client.send('250 2.1.0 Ok'); 383 | }); 384 | }; 385 | 386 | /** 387 | *

Add recipients to the e-mail envelope

388 | * 389 | *

This doesn't work if MAIL command is not yet executed

390 | * 391 | *

If validateRecipients option is set to true, then emits 392 | * 'validateRecipient' and wait for the callback before moving 393 | * on

394 | * 395 | * @param {String} mail Address payload in the form of 'TO:<address>' 396 | */ 397 | SMTPServerConnection.prototype._onCommandRCPT = function(mail) { 398 | var self = this, 399 | match, 400 | email, 401 | domain; 402 | 403 | if (!this.envelope.from) { 404 | return this.client.send('503 5.5.1 Error: need MAIL command'); 405 | } 406 | 407 | if (!(match = mail.match(/^to\:\s*<([^@>]+\@([^@>]+))>$/i))) { 408 | return this.client.send('501 5.1.7 Bad recipient address syntax'); 409 | } 410 | 411 | email = match[1] || ''; 412 | domain = (match[2] || '').toLowerCase(); 413 | 414 | this._validateAddress('recipient', email, domain, function(err) { 415 | if (err) { 416 | return self.client.send(err.message); 417 | } 418 | 419 | // force domain part to be lowercase 420 | email = email.substr(0, email.length - domain.length) + domain; 421 | 422 | // add to recipients list 423 | if (self.envelope.to.indexOf(email) < 0) { 424 | self.envelope.to.push(email); 425 | } 426 | self.client.send('250 2.1.0 Ok'); 427 | }); 428 | 429 | }; 430 | 431 | /** 432 | *

If disableDNSValidation option is set to false, then performs 433 | * validation via DNS lookup. 434 | * 435 | *

If validate{type} option is set to true, then emits 436 | * 'validate{type}' and waits for the callback before moving 437 | * on

438 | * 439 | * @param {String} addressType 'sender' or 'recipient' 440 | * @param {String} email 441 | * @param {String} domain 442 | * @param {Function} callback 443 | */ 444 | SMTPServerConnection.prototype._validateAddress = function(addressType, email, domain, callback) { 445 | 446 | var validateEvent, 447 | validationFailedEvent, 448 | dnsErrorMessage, 449 | localErrorMessage; 450 | 451 | if (addressType === 'sender') { 452 | validateEvent = 'validateSender'; 453 | validationFailedEvent = 'senderValidationFailed'; 454 | dnsErrorMessage = '450 4.1.8 <' + email + '>: Sender address rejected: Domain not found'; 455 | localErrorMessage = '550 5.1.1 <' + email + '>: Sender address rejected: User unknown in local sender table'; 456 | } else if (addressType === 'recipient') { 457 | validateEvent = 'validateRecipient'; 458 | validationFailedEvent = 'recipientValidationFailed'; 459 | dnsErrorMessage = '450 4.1.8 <' + email + '>: Recipient address rejected: Domain not found'; 460 | localErrorMessage = '550 5.1.1 <' + email + '>: Recipient address rejected: User unknown in local recipient table'; 461 | } else { 462 | // How are internal errors handled? 463 | throw new Error('Address type not supported'); 464 | } 465 | 466 | var validateViaLocal = function() { 467 | if (this.server.listeners(validateEvent).length) { 468 | this.server.emit(validateEvent, this.envelope, email, (function(err) { 469 | if (err) { 470 | return callback(new Error(err.SMTPResponse || localErrorMessage)); 471 | } 472 | return callback(); 473 | }).bind(this)); 474 | } else { 475 | return callback(); 476 | } 477 | }; 478 | 479 | var validateViaDNS = function() { 480 | dnslib.resolveMx(domain, (function(err, addresses) { 481 | if (err || !addresses || !addresses.length) { 482 | this.server.emit(validationFailedEvent, email); 483 | return callback(new Error(err && err.SMTPResponse || dnsErrorMessage)); 484 | } 485 | validateViaLocal.call(this); 486 | }).bind(this)); 487 | }; 488 | 489 | if (!this.server.options.disableDNSValidation) { 490 | validateViaDNS.call(this); 491 | } else { 492 | return validateViaLocal.call(this); 493 | } 494 | }; 495 | 496 | /** 497 | *

Switch to data mode and starts waiting for a binary data stream. Emits 498 | * 'startData'.

499 | * 500 | *

If RCPT is not yet run, stop

501 | */ 502 | SMTPServerConnection.prototype._onCommandDATA = function() { 503 | 504 | if (!this.envelope.to.length) { 505 | return this.client.send('503 5.5.1 Error: need RCPT command'); 506 | } 507 | 508 | this.client.startDataMode(); 509 | this.client.send('354 End data with .'); 510 | this.server.emit('startData', this.envelope); 511 | }; 512 | 513 | /** 514 | *

Resets the current state - e-mail data and authentication info

515 | */ 516 | SMTPServerConnection.prototype._onCommandRSET = function() { 517 | this.init(); 518 | this.client.send('250 2.0.0 Ok'); 519 | }; 520 | 521 | /** 522 | *

If the server is in secure connection mode, start the authentication 523 | * process. Param payload defines the authentication mechanism.

524 | * 525 | *

Currently supported - PLAIN and LOGIN. There is no need for more 526 | * complicated mechanisms (different CRAM versions etc.) since authentication 527 | * is only done in secure connection mode

528 | * 529 | * @param {Buffer} payload Defines the authentication mechanism 530 | */ 531 | SMTPServerConnection.prototype._onCommandAUTH = function(payload) { 532 | var method; 533 | 534 | if (!this.server.options.requireAuthentication && !this.server.options.enableAuthentication) { 535 | return this.client.send('503 5.5.1 Error: authentication not enabled'); 536 | } 537 | 538 | if (!this.server.options.ignoreTLS && !this.client.secureConnection) { 539 | return this.client.send('530 5.7.0 Must issue a STARTTLS command first'); 540 | } 541 | 542 | if (this.authentication.authenticated) { 543 | return this.client.send('503 5.7.0 No identity changes permitted'); 544 | } 545 | 546 | payload = payload.toString('utf-8').trim().split(' '); 547 | method = payload.shift().trim().toUpperCase(); 548 | 549 | if (this.server.options.authMethods.indexOf(method) < 0) { 550 | return this.client.send('535 5.7.8 Error: authentication failed: no mechanism available'); 551 | } 552 | 553 | switch (method) { 554 | case 'PLAIN': 555 | this._handleAuthPlain(payload); 556 | break; 557 | case 'XOAUTH2': 558 | this._handleAuthXOAuth2(payload); 559 | break; 560 | case 'LOGIN': 561 | var username = payload.shift(); 562 | if (username) { 563 | username = username.trim(); 564 | this.authentication.state = 'AUTHENTICATING'; 565 | } 566 | this._handleAuthLogin(username); 567 | break; 568 | } 569 | }; 570 | 571 | /** 572 | *

Upgrade the connection to a secure TLS connection

573 | */ 574 | SMTPServerConnection.prototype._onCommandSTARTTLS = function() { 575 | if(this.server.options.disableSTARTTLS) { 576 | return this.client.send('502 5.5.2 Error: command not recognized'); 577 | } 578 | if (this.client.secureConnection) { 579 | return this.client.send('554 5.5.1 Error: TLS already active'); 580 | } 581 | 582 | this.client.send('220 2.0.0 Ready to start TLS'); 583 | 584 | this.client.startTLS(this.server.options.credentials, (function() { 585 | // Connection secured 586 | // nothing to do here, since it is the client that should 587 | // make the next move 588 | }).bind(this)); 589 | }; 590 | 591 | /** 592 | *

Retrieve hostname from the client. Not very important, since client 593 | * IP is already known and the client can send fake data

594 | * 595 | * @param {String} host Hostname of the client 596 | */ 597 | SMTPServerConnection.prototype._onCommandHELO = function(host) { 598 | if (!host) { 599 | return this.client.send('501 Syntax: EHLO hostname'); 600 | } else { 601 | this.hostNameAppearsAs = host; 602 | this.envelope.host = host; 603 | } 604 | this.client.send('250 ' + this.server.options.name + ' at your service, [' + 605 | this.client.remoteAddress + ']'); 606 | }; 607 | 608 | /** 609 | *

Retrieve hostname from the client. Not very important, since client 610 | * IP is already known and the client can send fake data

611 | * 612 | *

Additionally displays server capability list to the client

613 | * 614 | * @param {String} host Hostname of the client 615 | */ 616 | SMTPServerConnection.prototype._onCommandEHLO = function(host) { 617 | var response = [this.server.options.name + ' at your service, [' + 618 | this.client.remoteAddress + ']', '8BITMIME', 'ENHANCEDSTATUSCODES' 619 | ]; 620 | 621 | if (this.server.options.maxSize) { 622 | response.push('SIZE ' + this.server.options.maxSize); 623 | } 624 | 625 | if ((this.client.secureConnection || this.server.options.ignoreTLS) && (this.server.options.requireAuthentication || this.server.options.enableAuthentication)) { 626 | response.push('AUTH ' + this.server.options.authMethods.join(' ')); 627 | response.push('AUTH=' + this.server.options.authMethods.join(' ')); 628 | } 629 | 630 | if (!this.client.secureConnection && !this.server.options.disableSTARTTLS) { 631 | response.push('STARTTLS'); 632 | } 633 | 634 | if (!host) { 635 | return this.client.send('501 Syntax: EHLO hostname'); 636 | } else { 637 | this.hostNameAppearsAs = host; 638 | this.envelope.host = host; 639 | } 640 | 641 | this.client.send(response.map(function(feature, i, arr) { 642 | return '250' + (i < arr.length - 1 ? '-' : ' ') + feature; 643 | }).join('\r\n')); 644 | }; 645 | 646 | /** 647 | *

No operation. Just returns OK.

648 | */ 649 | SMTPServerConnection.prototype._onCommandNOOP = function() { 650 | this.client.send('250 OK'); 651 | }; 652 | 653 | /** 654 | *

Detect login information from the payload and initiate authentication 655 | * by emitting 'authorizeUser' and waiting for its callback

656 | * 657 | * @param {Buffer} payload AUTH PLAIN login information 658 | */ 659 | SMTPServerConnection.prototype._handleAuthPlain = function(payload) { 660 | if (payload.length) { 661 | var userdata = new Buffer(payload.join(' '), 'base64'), 662 | password; 663 | userdata = userdata.toString('utf-8').split('\u0000'); 664 | 665 | if (userdata.length != 3) { 666 | return this.client.send('500 5.5.2 Error: invalid userdata to decode'); 667 | } 668 | 669 | this.authentication.username = userdata[1] || userdata[0] || ''; 670 | password = userdata[2] || ''; 671 | 672 | this.server.emit('authorizeUser', 673 | this.envelope, 674 | this.authentication.username, 675 | password, (function(err, success) { 676 | if (err || !success) { 677 | this.authentication.authenticated = false; 678 | this.authentication.username = false; 679 | this.authentication.state = 'NORMAL'; 680 | return this.client.send('535 5.7.8 Error: authentication failed: generic failure'); 681 | } 682 | this.client.send('235 2.7.0 Authentication successful'); 683 | this.authentication.authenticated = true; 684 | this.authentication.state = 'AUTHENTICATED'; 685 | }).bind(this)); 686 | } else { 687 | if (this.authentication.state == 'NORMAL') { 688 | this.authentication.state = 'AUTHPLAINUSERDATA'; 689 | this.client.send('334'); 690 | } 691 | } 692 | }; 693 | 694 | /** 695 | *

Sets authorization state to 'AUTHENTICATING' and reuqests for the 696 | * username and password from the client

697 | * 698 | *

If username and password are set initiate authentication 699 | * by emitting 'authorizeUser' and waiting for its callback

700 | * 701 | * @param {Buffer} payload AUTH LOGIN login information 702 | */ 703 | SMTPServerConnection.prototype._handleAuthLogin = function(payload) { 704 | if (this.authentication.state == 'NORMAL') { 705 | this.authentication.state = 'AUTHENTICATING'; 706 | this.client.send('334 VXNlcm5hbWU6'); 707 | } else if (this.authentication.state == 'AUTHENTICATING') { 708 | if (this.authentication.username === false) { 709 | this.authentication.username = new Buffer(payload, 'base64').toString('utf-8'); 710 | this.client.send('334 UGFzc3dvcmQ6'); 711 | } else { 712 | this.authentication.state = 'VERIFYING'; 713 | this.server.emit('authorizeUser', 714 | this.envelope, 715 | this.authentication.username, 716 | new Buffer(payload, 'base64').toString('utf-8'), (function(err, success) { 717 | if (err || !success) { 718 | this.authentication.authenticated = false; 719 | this.authentication.username = false; 720 | this.authentication.state = 'NORMAL'; 721 | return this.client.send('535 5.7.8 Error: authentication failed: generic failure'); 722 | } 723 | this.client.send('235 2.7.0 Authentication successful'); 724 | this.authentication.authenticated = true; 725 | this.authentication.state = 'AUTHENTICATED'; 726 | }).bind(this)); 727 | } 728 | 729 | } 730 | }; 731 | 732 | /** 733 | *

Detect login information from the payload and initiate authentication 734 | * by emitting 'authorizeUser' and waiting for its callback

735 | * 736 | * @param {Buffer} payload AUTH XOAUTH2 login information 737 | */ 738 | SMTPServerConnection.prototype._handleAuthXOAuth2 = function(payload) { 739 | if (this.authentication.state == 'AUTHXOAUTH2') { 740 | // empty response from the client 741 | this.authentication.authenticated = false; 742 | this.authentication.username = false; 743 | this.authentication.state = 'NORMAL'; 744 | return this.client.send('535 5.7.1 Username and Password not accepted'); 745 | } 746 | 747 | var userdata = new Buffer(payload.join(' '), 'base64'), 748 | token; 749 | userdata = userdata.toString('utf-8').split('\x01'); 750 | 751 | if (userdata.length != 4) { 752 | return this.client.send('500 5.5.2 Error: invalid userdata to decode'); 753 | } 754 | 755 | this.authentication.username = userdata[0].substr(5) || ''; 756 | token = userdata[1].split(' ')[1] || ''; 757 | 758 | this.server.emit('authorizeUser', 759 | this.envelope, 760 | this.authentication.username, 761 | token, (function(err, success) { 762 | if (err || !success) { 763 | this.authentication.state = 'AUTHXOAUTH2'; 764 | return this.client.send('334 eyJzdGF0dXMiOiI0MDEiLCJzY2hlbWVzIjoiYmVhcmVyIG1hYyIsInNjb3BlIjoiaHR0cHM6Ly9tYWlsLmdvb2dsZS5jb20vIn0K'); 765 | } 766 | this.client.send('235 2.7.0 Authentication successful'); 767 | this.authentication.authenticated = true; 768 | this.authentication.state = 'AUTHENTICATED'; 769 | }).bind(this)); 770 | }; 771 | 772 | /** 773 | *

Emits the data received from the client with 'data' 774 | * 775 | * @event 776 | * @param {Buffer} chunk Binary data sent by the client on data mode 777 | */ 778 | SMTPServerConnection.prototype._onData = function(chunk) { 779 | this.server.emit('data', this.envelope, chunk); 780 | }; 781 | 782 | /** 783 | *

If the data stream ends, emit 'dataReady'and wait for 784 | * the callback, only if server listened for it.

785 | * 786 | * @event 787 | */ 788 | SMTPServerConnection.prototype._onDataReady = function() { 789 | if (this.server.listeners('dataReady').length) { 790 | this.server.emit('dataReady', this.envelope, (function(err, code) { 791 | this.init(true); //reset state, keep auth data 792 | 793 | if (err) { 794 | this.client.send(err && err.SMTPResponse || ('550 ' + (err && err.message || 'FAILED'))); 795 | } else { 796 | this.client.send('250 2.0.0 Ok: queued as ' + (code || crypto.randomBytes(10).toString('hex'))); 797 | } 798 | 799 | }).bind(this)); 800 | } else { 801 | this.init(true); //reset state, keep auth data 802 | this.client.send('250 2.0.0 Ok: queued as ' + crypto.randomBytes(10).toString('hex')); 803 | } 804 | }; -------------------------------------------------------------------------------- /lib/simpleserver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createSMTPServer = require('./server'), 4 | Stream = require('stream').Stream, 5 | utillib = require('util'), 6 | oslib = require('os'); 7 | 8 | module.exports = function(options, connectionCallback) { 9 | return new SimpleServer(options, connectionCallback); 10 | }; 11 | 12 | function SimpleServer(options, connectionCallback) { 13 | if (!connectionCallback && typeof options == 'function') { 14 | connectionCallback = options; 15 | options = undefined; 16 | } 17 | 18 | this.connectionCallback = connectionCallback; 19 | 20 | this.options = options || {}; 21 | this.initialChunk = true; 22 | 23 | if (!('ignoreTLS' in this.options)) { 24 | this.options.ignoreTLS = true; 25 | } 26 | 27 | if (!('disableDNSValidation' in this.options)) { 28 | this.options.disableDNSValidation = true; 29 | } 30 | 31 | this.server = createSMTPServer(options); 32 | this.listen = this.server.listen.bind(this.server); 33 | 34 | this.server.on('startData', this._onStartData.bind(this)); 35 | this.server.on('data', this._onData.bind(this)); 36 | this.server.on('dataReady', this._onDataReady.bind(this)); 37 | } 38 | 39 | SimpleServer.prototype._onStartData = function(connection) { 40 | connection._session = new SimpleServerConnection(connection); 41 | this.connectionCallback(connection._session); 42 | }; 43 | 44 | SimpleServer.prototype._onData = function(connection, chunk) { 45 | if (this.initialChunk) { 46 | chunk = Buffer.concat([new Buffer(this._generateReceivedHeader(connection) + '\r\n', 'utf-8'), chunk]); 47 | this.initialChunk = false; 48 | } 49 | connection._session.emit('data', chunk); 50 | }; 51 | 52 | SimpleServer.prototype._onDataReady = function(connection, callback) { 53 | connection._session._setCallback(callback); 54 | connection._session.emit('end'); 55 | }; 56 | 57 | SimpleServer.prototype._generateReceivedHeader = function(connection) { 58 | var parts = []; 59 | 60 | if (connection.host && !connection.host.match(/^\[?\d+\.\d+\.\d+\.\d+\]?$/)) { 61 | parts.push('from ' + connection.host); 62 | parts.push('(' + connection.remoteAddress + ')'); 63 | } else { 64 | parts.push('from ' + connection.remoteAddress); 65 | } 66 | 67 | parts.push('by ' + getHostName()); 68 | 69 | parts.push('with SMTP;'); 70 | 71 | parts.push(Date()); 72 | 73 | return 'Received: ' + parts.join(' '); 74 | }; 75 | 76 | function SimpleServerConnection(connection) { 77 | Stream.call(this); 78 | 79 | this.accepted = false; 80 | this.rejected = false; 81 | 82 | this._callback = (function(err, code) { 83 | if (err) { 84 | this.rejected = err; 85 | } else { 86 | this.accepted = code || true; 87 | } 88 | }); 89 | 90 | ['from', 'to', 'host', 'remoteAddress'].forEach((function(key) { 91 | if (connection[key]) { 92 | this[key] = connection[key]; 93 | } 94 | }).bind(this)); 95 | } 96 | utillib.inherits(SimpleServerConnection, Stream); 97 | 98 | SimpleServerConnection.prototype._setCallback = function(callback) { 99 | 100 | if (this.rejected) { 101 | return callback(this.rejected); 102 | } else if (this.accepted) { 103 | return callback(null, this.accepted !== true ? this.accepted : undefined); 104 | } else { 105 | this._callback = callback; 106 | } 107 | 108 | }; 109 | 110 | SimpleServerConnection.prototype.pause = function() {}; 111 | 112 | SimpleServerConnection.prototype.resume = function() {}; 113 | 114 | SimpleServerConnection.prototype.accept = function(code) { 115 | this._callback(null, code); 116 | }; 117 | 118 | SimpleServerConnection.prototype.reject = function(reason) { 119 | this._callback(new Error(reason || 'Rejected')); 120 | }; 121 | 122 | function getHostName() { 123 | return (oslib.hostname && oslib.hostname()) || 124 | (oslib.getHostname && oslib.getHostname()) || 125 | '127.0.0.1'; 126 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplesmtp", 3 | "description": "Simple SMTP server module to create custom SMTP servers", 4 | "version": "0.3.35", 5 | "author" : "Andris Reinman", 6 | "maintainers":[ 7 | { 8 | "name":"andris", 9 | "email":"andris@node.ee" 10 | } 11 | ], 12 | "repository" : { 13 | "type" : "git", 14 | "url" : "http://github.com/andris9/simplesmtp.git" 15 | }, 16 | "scripts":{ 17 | "test": "nodeunit test/" 18 | }, 19 | "main" : "./lib/smtp", 20 | "licenses" : [ 21 | { 22 | "type": "MIT", 23 | "url": "http://github.com/andris9/simplesmtp/blob/master/LICENSE" 24 | } 25 | ], 26 | "dependencies": { 27 | "rai": "~0.1.11", 28 | "xoauth2": "~0.1.8" 29 | }, 30 | "devDependencies": { 31 | "nodeunit": "*", 32 | "mailcomposer": "*" 33 | }, 34 | "engines" : { "node" : ">=0.8.0" }, 35 | "keywords": ["servers", "text-based", "smtp", "email", "mail", "e-mail"] 36 | } 37 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var simplesmtp = require('../index'), 4 | packageData = require('../package.json'), 5 | fs = require('fs'); 6 | 7 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 8 | 9 | var PORT_NUMBER = 8397; 10 | 11 | exports['Version test'] = { 12 | 'Should expose version number': function(test) { 13 | test.ok(simplesmtp.version); 14 | test.equal(simplesmtp.version, packageData.version); 15 | test.done(); 16 | } 17 | }; 18 | 19 | exports['General tests'] = { 20 | setUp: function(callback) { 21 | this.server = new simplesmtp.createServer(); 22 | this.server.listen(PORT_NUMBER, function(err) { 23 | if (err) { 24 | throw err; 25 | } else { 26 | callback(); 27 | } 28 | }); 29 | 30 | }, 31 | 32 | tearDown: function(callback) { 33 | this.server.end(callback); 34 | }, 35 | 36 | 'Connect and setup': function(test) { 37 | var client = simplesmtp.connect(PORT_NUMBER); 38 | 39 | client.once('idle', function() { 40 | // Client is ready to take messages 41 | test.ok(true); 42 | client.close(); 43 | }); 44 | 45 | client.on('error', function() { 46 | test.ok(false); 47 | }); 48 | 49 | client.on('end', function() { 50 | test.done(); 51 | }); 52 | }, 53 | 54 | 'socketTimeout': function(test) { 55 | var client = simplesmtp.connect(PORT_NUMBER, false, { 56 | socketTimeout: 500 57 | }); 58 | 59 | var waitTimeout = setTimeout(function() { 60 | test.ok(false); 61 | test.done(); 62 | }, 2000); 63 | 64 | client.once('idle', function() { 65 | // Client is ready to take messages 66 | test.ok(true); 67 | }); 68 | 69 | client.on('error', function(err) { 70 | test.ifError(err); 71 | }); 72 | 73 | client.on('end', function() { 74 | clearTimeout(waitTimeout); 75 | test.done(); 76 | }); 77 | } 78 | }; 79 | 80 | exports['Secure server'] = { 81 | setUp: function(callback) { 82 | this.server = new simplesmtp.createServer({ 83 | secureConnection: true 84 | }); 85 | this.server.listen(PORT_NUMBER, function(err) { 86 | if (err) { 87 | throw err; 88 | } else { 89 | callback(); 90 | } 91 | }); 92 | 93 | }, 94 | 95 | tearDown: function(callback) { 96 | this.server.end(callback); 97 | }, 98 | 99 | 'Connect and setup': function(test) { 100 | var client = simplesmtp.connect(PORT_NUMBER, false, { 101 | secureConnection: true 102 | }); 103 | 104 | client.once('idle', function() { 105 | // Client is ready to take messages 106 | test.ok(true); 107 | client.close(); 108 | }); 109 | 110 | client.on('error', function() { 111 | test.ok(false); 112 | }); 113 | 114 | client.on('end', function() { 115 | test.done(); 116 | }); 117 | }, 118 | 119 | 'Unsecure client should have timeout': function(test) { 120 | var client = simplesmtp.connect(PORT_NUMBER, false, { 121 | secureConnection: false 122 | }); 123 | 124 | client.once('idle', function() { 125 | test.ok(false); 126 | }); 127 | 128 | client.on('error', function(err) { 129 | test.equal(err.code, 'ETIMEDOUT'); 130 | client.close(); 131 | }); 132 | 133 | client.on('end', function() { 134 | test.done(); 135 | }); 136 | } 137 | }; 138 | 139 | exports['Disabled EHLO'] = { 140 | setUp: function(callback) { 141 | this.server = new simplesmtp.createServer({ 142 | disableEHLO: true 143 | }); 144 | this.server.listen(PORT_NUMBER, function(err) { 145 | if (err) { 146 | throw err; 147 | } else { 148 | callback(); 149 | } 150 | }); 151 | 152 | }, 153 | 154 | tearDown: function(callback) { 155 | this.server.end(callback); 156 | }, 157 | 158 | 'Connect and setup': function(test) { 159 | var client = simplesmtp.connect(PORT_NUMBER, false, {}); 160 | 161 | client.once('idle', function() { 162 | // Client is ready to take messages 163 | test.ok(true); 164 | client.close(); 165 | }); 166 | 167 | client.on('error', function() { 168 | test.ok(false); 169 | }); 170 | 171 | client.on('end', function() { 172 | test.done(); 173 | }); 174 | } 175 | }; 176 | 177 | exports['Authentication needed'] = { 178 | setUp: function(callback) { 179 | this.server = new simplesmtp.createServer({ 180 | requireAuthentication: true 181 | }); 182 | 183 | this.server.on('authorizeUser', function(envelope, user, pass, callback) { 184 | callback(null, user == 'test1' && pass == 'test2'); 185 | }); 186 | 187 | this.server.listen(PORT_NUMBER, function(err) { 188 | if (err) { 189 | throw err; 190 | } else { 191 | callback(); 192 | } 193 | }); 194 | 195 | }, 196 | 197 | tearDown: function(callback) { 198 | this.server.end(callback); 199 | }, 200 | 201 | 'Auth success': function(test) { 202 | var client = simplesmtp.connect(PORT_NUMBER, false, { 203 | auth: { 204 | user: 'test1', 205 | pass: 'test2' 206 | } 207 | }); 208 | 209 | client.once('idle', function() { 210 | // Client is ready to take messages 211 | test.ok(true); 212 | client.close(); 213 | }); 214 | 215 | client.on('error', function() { 216 | test.ok(false); 217 | }); 218 | 219 | client.on('end', function() { 220 | test.done(); 221 | }); 222 | }, 223 | 224 | 'Auth fails': function(test) { 225 | var client = simplesmtp.connect(PORT_NUMBER, false, { 226 | auth: { 227 | user: 'test3', 228 | pass: 'test4' 229 | } 230 | }); 231 | 232 | client.once('idle', function() { 233 | // Client is ready to take messages 234 | test.ok(false); // should not occur 235 | client.close(); 236 | }); 237 | 238 | client.on('error', function() { 239 | test.ok(true); // login failed 240 | }); 241 | 242 | client.on('end', function() { 243 | test.done(); 244 | }); 245 | } 246 | }; 247 | 248 | exports['Message tests'] = { 249 | setUp: function(callback) { 250 | this.server = new simplesmtp.createServer({ 251 | validateSender: true, 252 | validateRecipients: true 253 | }); 254 | 255 | this.server.on('validateSender', function(envelope, email, callback) { 256 | callback(email != 'test@pangalink.net' ? new Error('Failed sender') : null); 257 | }); 258 | 259 | this.server.on('validateRecipient', function(envelope, email, callback) { 260 | callback(email.split('@').pop() != 'pangalink.net' ? new Error('Failed recipient') : null); 261 | }); 262 | 263 | this.server.on('dataReady', function(envelope, callback) { 264 | callback(null, 'ABC1'); // ABC1 is the queue id to be advertised to the client 265 | // callback(new Error('That was clearly a spam!')); 266 | }); 267 | 268 | this.server.listen(PORT_NUMBER, function(err) { 269 | if (err) { 270 | throw err; 271 | } else { 272 | callback(); 273 | } 274 | }); 275 | 276 | }, 277 | 278 | tearDown: function(callback) { 279 | this.server.end(callback); 280 | }, 281 | 282 | 'Set envelope success': function(test) { 283 | test.expect(2); 284 | 285 | var client = simplesmtp.connect(PORT_NUMBER, false, {}); 286 | 287 | client.once('idle', function() { 288 | // Client is ready to take messages 289 | test.ok(true); // waiting for envelope 290 | 291 | client.useEnvelope({ 292 | from: 'test@pangalink.net', 293 | to: [ 294 | 'test1@pangalink.net', 295 | 'test2@pangalink.net' 296 | ] 297 | }); 298 | }); 299 | 300 | client.on('message', function() { 301 | // Client is ready to take messages 302 | test.ok(true); // waiting for message 303 | client.close(); 304 | }); 305 | 306 | client.on('error', function() { 307 | test.ok(false); 308 | }); 309 | 310 | client.on('end', function() { 311 | test.done(); 312 | }); 313 | }, 314 | 315 | 'Set envelope fails for sender': function(test) { 316 | test.expect(2); 317 | 318 | var client = simplesmtp.connect(PORT_NUMBER, false, {}); 319 | 320 | client.once('idle', function() { 321 | // Client is ready to take messages 322 | test.ok(true); // waiting for envelope 323 | 324 | client.useEnvelope({ 325 | from: 'test3@pangalink.net', 326 | to: [ 327 | 'test1@pangalink.net', 328 | 'test2@pangalink.net' 329 | ] 330 | }); 331 | }); 332 | 333 | client.on('message', function() { 334 | // Client is ready to take messages 335 | test.ok(false); // waiting for message 336 | client.close(); 337 | }); 338 | 339 | client.on('error', function() { 340 | test.ok(true); 341 | }); 342 | 343 | client.on('end', function() { 344 | test.done(); 345 | }); 346 | }, 347 | 348 | 'Set envelope fails for receiver': function(test) { 349 | test.expect(2); 350 | 351 | var client = simplesmtp.connect(PORT_NUMBER, false, {}); 352 | 353 | client.once('idle', function() { 354 | // Client is ready to take messages 355 | test.ok(true); // waiting for envelope 356 | 357 | client.useEnvelope({ 358 | from: 'test@pangalink.net', 359 | to: [ 360 | 'test1@kreata.ee', 361 | 'test2@kreata.ee' 362 | ] 363 | }); 364 | }); 365 | 366 | client.on('message', function() { 367 | // Client is ready to take messages 368 | test.ok(false); // waiting for message 369 | client.close(); 370 | }); 371 | 372 | client.on('error', function() { 373 | test.ok(true); 374 | }); 375 | 376 | client.on('end', function() { 377 | test.done(); 378 | }); 379 | }, 380 | 381 | 'Set envelope partly fails': function(test) { 382 | test.expect(3); 383 | 384 | var client = simplesmtp.connect(PORT_NUMBER, false, {}); 385 | 386 | client.once('idle', function() { 387 | // Client is ready to take messages 388 | test.ok(true); // waiting for envelope 389 | 390 | client.useEnvelope({ 391 | from: 'test@pangalink.net', 392 | to: [ 393 | 'test1@pangalink.net', 394 | 'test2@kreata.ee' 395 | ] 396 | }); 397 | }); 398 | 399 | client.on('rcptFailed', function() { 400 | // Client is ready to take messages 401 | test.ok(true); // waiting for message 402 | }); 403 | 404 | client.on('message', function() { 405 | // Client is ready to take messages 406 | test.ok(true); // waiting for message 407 | client.close(); 408 | }); 409 | 410 | client.on('error', function() { 411 | test.ok(false); 412 | }); 413 | 414 | client.on('end', function() { 415 | test.done(); 416 | }); 417 | }, 418 | 419 | 'Send message success': function(test) { 420 | test.expect(3); 421 | 422 | var client = simplesmtp.connect(PORT_NUMBER, false, {}); 423 | 424 | client.once('idle', function() { 425 | // Client is ready to take messages 426 | test.ok(true); // waiting for envelope 427 | 428 | client.useEnvelope({ 429 | from: 'test@pangalink.net', 430 | to: [ 431 | 'test1@pangalink.net', 432 | 'test2@pangalink.net' 433 | ] 434 | }); 435 | }); 436 | 437 | client.on('message', function() { 438 | // Client is ready to take messages 439 | test.ok(true); // waiting for message 440 | 441 | client.write('From: abc@pangalink.net\r\nTo:cde@pangalink.net\r\nSubject: test\r\n\r\nHello World!'); 442 | client.end(); 443 | }); 444 | 445 | client.on('ready', function(success) { 446 | test.ok(success); 447 | client.close(); 448 | }); 449 | 450 | client.on('error', function() { 451 | test.ok(false); 452 | }); 453 | 454 | client.on('end', function() { 455 | test.done(); 456 | }); 457 | }, 458 | 459 | 'Stream message': function(test) { 460 | test.expect(3); 461 | 462 | var client = simplesmtp.connect(PORT_NUMBER, false, {}); 463 | 464 | client.once('idle', function() { 465 | // Client is ready to take messages 466 | test.ok(true); // waiting for envelope 467 | 468 | client.useEnvelope({ 469 | from: 'test@pangalink.net', 470 | to: [ 471 | 'test1@pangalink.net', 472 | 'test2@pangalink.net' 473 | ] 474 | }); 475 | }); 476 | 477 | client.on('message', function() { 478 | // Client is ready to take messages 479 | test.ok(true); // waiting for message 480 | 481 | // pipe file to client 482 | fs.createReadStream(__dirname + '/testmessage.eml').pipe(client); 483 | }); 484 | 485 | client.on('ready', function(success) { 486 | test.ok(success); 487 | client.close(); 488 | }); 489 | 490 | client.on('error', function() { 491 | test.ok(false); 492 | }); 493 | 494 | client.on('end', function() { 495 | test.done(); 496 | }); 497 | } 498 | }; -------------------------------------------------------------------------------- /test/pool.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint loopfunc: true */ 4 | 5 | var simplesmtp = require('../index'), 6 | MailComposer = require('mailcomposer').MailComposer; 7 | 8 | var PORT_NUMBER = 8397; 9 | 10 | exports['General tests'] = { 11 | setUp: function(callback) { 12 | this.server = new simplesmtp.createServer({}); 13 | this.server.listen(PORT_NUMBER, function(err) { 14 | if (err) { 15 | throw err; 16 | } else { 17 | callback(); 18 | } 19 | }); 20 | 21 | }, 22 | 23 | tearDown: function(callback) { 24 | this.server.end(callback); 25 | }, 26 | 27 | 'Send single message': function(test) { 28 | 29 | var pool = simplesmtp.createClientPool(PORT_NUMBER), 30 | mc = new MailComposer({ 31 | escapeSMTP: true 32 | }); 33 | 34 | mc.setMessageOption({ 35 | from: 'andmekala@hot.ee', 36 | to: 'andris@pangalink.net', 37 | subject: 'Hello!', 38 | body: 'Hello world!', 39 | html: 'Hello world!' 40 | }); 41 | 42 | this.server.on('dataReady', function(envelope, callback) { 43 | test.ok(true); 44 | callback(); 45 | }); 46 | 47 | pool.sendMail(mc, function(error) { 48 | test.ifError(error); 49 | pool.close(function() { 50 | test.ok(true); 51 | test.done(); 52 | }); 53 | }); 54 | }, 55 | 56 | 'Send several messages': function(test) { 57 | var total = 10; 58 | 59 | test.expect(total * 2); 60 | 61 | var pool = simplesmtp.createClientPool(PORT_NUMBER), 62 | mc; 63 | 64 | this.server.on('dataReady', function(envelope, callback) { 65 | process.nextTick(callback); 66 | }); 67 | 68 | var completed = 0; 69 | for (var i = 0; i < total; i++) { 70 | mc = new MailComposer({ 71 | escapeSMTP: true 72 | }); 73 | mc.setMessageOption({ 74 | from: 'andmekala@hot.ee', 75 | to: 'andris@pangalink.net', 76 | subject: 'Hello!', 77 | body: 'Hello world!', 78 | html: 'Hello world!' 79 | }); 80 | pool.sendMail(mc, function(error) { 81 | test.ifError(error); 82 | test.ok(true); 83 | completed++; 84 | if (completed >= total) { 85 | pool.close(function() { 86 | test.done(); 87 | }); 88 | } 89 | }); 90 | } 91 | }, 92 | 93 | 'Delivery error once': function(test) { 94 | 95 | var pool = simplesmtp.createClientPool(PORT_NUMBER), 96 | mc = new MailComposer({ 97 | escapeSMTP: true 98 | }); 99 | 100 | mc.setMessageOption({ 101 | from: 'andmekala@hot.ee', 102 | to: 'andris@pangalink.net', 103 | subject: 'Hello!', 104 | body: 'Hello world!', 105 | html: 'Hello world!' 106 | }); 107 | 108 | this.server.on('dataReady', function(envelope, callback) { 109 | test.ok(true); 110 | callback(new Error('Spam!')); 111 | }); 112 | 113 | pool.sendMail(mc, function(error) { 114 | test.equal(error && error.name, 'DeliveryError'); 115 | pool.close(function() { 116 | test.ok(true); 117 | test.done(); 118 | }); 119 | }); 120 | }, 121 | 122 | 'Delivery error several times': function(test) { 123 | var total = 10; 124 | 125 | test.expect(total); 126 | 127 | var pool = simplesmtp.createClientPool(PORT_NUMBER), 128 | mc; 129 | 130 | this.server.on('dataReady', function(envelope, callback) { 131 | process.nextTick(function() { 132 | callback(new Error('Spam!')); 133 | }); 134 | }); 135 | 136 | var completed = 0; 137 | for (var i = 0; i < total; i++) { 138 | mc = new MailComposer({ 139 | escapeSMTP: true 140 | }); 141 | mc.setMessageOption({ 142 | from: 'andmekala@hot.ee', 143 | to: 'andris@pangalink.net', 144 | subject: 'Hello!', 145 | body: 'Hello world!', 146 | html: 'Hello world!' 147 | }); 148 | 149 | pool.sendMail(mc, function(error) { 150 | test.equal(error && error.name, 'DeliveryError'); 151 | completed++; 152 | if (completed >= total) { 153 | pool.close(function() { 154 | test.done(); 155 | }); 156 | } 157 | }); 158 | } 159 | } 160 | }; 161 | 162 | exports['Auth fail tests'] = { 163 | setUp: function(callback) { 164 | this.server = new simplesmtp.createServer({ 165 | requireAuthentication: true 166 | }); 167 | 168 | this.server.listen(PORT_NUMBER, function(err) { 169 | if (err) { 170 | throw err; 171 | } else { 172 | callback(); 173 | } 174 | }); 175 | 176 | this.server.on('authorizeUser', function(envelope, username, password, callback) { 177 | callback(null, username == password); 178 | }); 179 | }, 180 | 181 | tearDown: function(callback) { 182 | this.server.end(callback); 183 | }, 184 | 185 | 'Authentication passes once': function(test) { 186 | var pool = simplesmtp.createClientPool(PORT_NUMBER, false, { 187 | auth: { 188 | 'user': 'test', 189 | 'pass': 'test' 190 | } 191 | }), 192 | mc = new MailComposer({ 193 | escapeSMTP: true 194 | }); 195 | 196 | mc.setMessageOption({ 197 | from: 'andmekala2@hot.ee', 198 | to: 'andris2@pangalink.net', 199 | subject: 'Hello2!', 200 | body: 'Hello2 world!', 201 | html: 'Hello2 world!' 202 | }); 203 | 204 | this.server.on('dataReady', function(envelope, callback) { 205 | test.ok(true); 206 | callback(); 207 | }); 208 | 209 | pool.sendMail(mc, function(error) { 210 | test.ifError(error); 211 | pool.close(function() { 212 | test.ok(true); 213 | test.done(); 214 | }); 215 | }); 216 | 217 | }, 218 | 219 | 'Authentication error once': function(test) { 220 | var pool = simplesmtp.createClientPool(PORT_NUMBER, false, { 221 | auth: { 222 | 'user': 'test1', 223 | 'pass': 'test2' 224 | } 225 | }), 226 | mc = new MailComposer({ 227 | escapeSMTP: true 228 | }); 229 | 230 | mc.setMessageOption({ 231 | from: 'andmekala2@hot.ee', 232 | to: 'andris2@pangalink.net', 233 | subject: 'Hello2!', 234 | body: 'Hello2 world!', 235 | html: 'Hello2 world!' 236 | }); 237 | 238 | this.server.on('dataReady', function(envelope, callback) { 239 | test.ok(true); 240 | callback(); 241 | }); 242 | 243 | pool.sendMail(mc, function(error) { 244 | test.equal(error && error.name, 'AuthError'); 245 | pool.close(function() { 246 | test.ok(true); 247 | test.done(); 248 | }); 249 | }); 250 | } 251 | }; 252 | 253 | exports['Max messages'] = { 254 | setUp: function(callback) { 255 | this.server = new simplesmtp.createServer({}); 256 | this.server.listen(PORT_NUMBER, function(err) { 257 | if (err) { 258 | throw err; 259 | } else { 260 | callback(); 261 | } 262 | }); 263 | 264 | }, 265 | 266 | tearDown: function(callback) { 267 | this.server.end(callback); 268 | }, 269 | 270 | 'Limit 1': function(test) { 271 | var total = 10; 272 | 273 | test.expect(total * 3); 274 | 275 | var pool = simplesmtp.createClientPool(PORT_NUMBER, false, { 276 | maxMessages: 1, 277 | maxConnections: 1 278 | }), 279 | mc; 280 | 281 | pool.on('released', function() { 282 | test.ok(1); 283 | }); 284 | 285 | this.server.on('dataReady', function(envelope, callback) { 286 | process.nextTick(callback); 287 | }); 288 | 289 | var completed = 0; 290 | for (var i = 0; i < total; i++) { 291 | mc = new MailComposer({ 292 | escapeSMTP: true 293 | }); 294 | mc.setMessageOption({ 295 | from: 'andmekala@hot.ee', 296 | to: 'andris@pangalink.net', 297 | subject: 'Hello!', 298 | body: 'Hello world!', 299 | html: 'Hello world!' 300 | }); 301 | pool.sendMail(mc, function(error) { 302 | test.ifError(error); 303 | test.ok(true); 304 | completed++; 305 | if (completed >= total) { 306 | pool.close(function() { 307 | test.done(); 308 | }); 309 | } 310 | }); 311 | } 312 | }, 313 | 314 | 'Limit 2': function(test) { 315 | var total = 10; 316 | 317 | test.expect(total * 2 + 5); 318 | 319 | var pool = simplesmtp.createClientPool(PORT_NUMBER, false, { 320 | maxMessages: 2, 321 | maxConnections: 1 322 | }), 323 | mc; 324 | 325 | pool.on('released', function() { 326 | test.ok(1); 327 | }); 328 | 329 | this.server.on('dataReady', function(envelope, callback) { 330 | process.nextTick(callback); 331 | }); 332 | 333 | var completed = 0; 334 | for (var i = 0; i < total; i++) { 335 | mc = new MailComposer({ 336 | escapeSMTP: true 337 | }); 338 | mc.setMessageOption({ 339 | from: 'andmekala@hot.ee', 340 | to: 'andris@pangalink.net', 341 | subject: 'Hello!', 342 | body: 'Hello world!', 343 | html: 'Hello world!' 344 | }); 345 | pool.sendMail(mc, function(error) { 346 | test.ifError(error); 347 | test.ok(true); 348 | completed++; 349 | if (completed >= total) { 350 | pool.close(function() { 351 | test.done(); 352 | }); 353 | } 354 | }); 355 | } 356 | }, 357 | 358 | 'No limit': function(test) { 359 | var total = 10; 360 | 361 | test.expect(total * 2); 362 | 363 | var pool = simplesmtp.createClientPool(PORT_NUMBER, false, { 364 | maxConnections: 1 365 | }), 366 | mc; 367 | 368 | pool.on('released', function() { 369 | test.ok(1); 370 | }); 371 | 372 | this.server.on('dataReady', function(envelope, callback) { 373 | process.nextTick(callback); 374 | }); 375 | 376 | var completed = 0; 377 | for (var i = 0; i < total; i++) { 378 | mc = new MailComposer({ 379 | escapeSMTP: true 380 | }); 381 | mc.setMessageOption({ 382 | from: 'andmekala@hot.ee', 383 | to: 'andris@pangalink.net', 384 | subject: 'Hello!', 385 | body: 'Hello world!', 386 | html: 'Hello world!' 387 | }); 388 | pool.sendMail(mc, function(error) { 389 | test.ifError(error); 390 | test.ok(true); 391 | completed++; 392 | if (completed >= total) { 393 | pool.close(function() { 394 | test.done(); 395 | }); 396 | } 397 | }); 398 | } 399 | } 400 | }; -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var runClientMockup = require('rai').runClientMockup, 4 | simplesmtp = require('../index'), 5 | netlib = require('net'); 6 | 7 | var PORT_NUMBER = 8397; 8 | 9 | // monkey patch net and tls to support nodejs 0.4 10 | if (!netlib.connect && netlib.createConnection) { 11 | netlib.connect = netlib.createConnection; 12 | } 13 | 14 | exports['General tests'] = { 15 | setUp: function(callback) { 16 | 17 | this.smtp = new simplesmtp.createServer({ 18 | SMTPBanner: 'SCORPIO', 19 | name: 'MYRDO', 20 | maxSize: 1234, 21 | maxClients: 1, 22 | }); 23 | this.smtp.listen(PORT_NUMBER, function(err) { 24 | if (err) { 25 | throw err; 26 | } else { 27 | callback(); 28 | } 29 | }); 30 | 31 | }, 32 | tearDown: function(callback) { 33 | this.smtp.end(callback); 34 | }, 35 | 'QUIT': function(test) { 36 | var cmds = ['QUIT']; 37 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 38 | test.equal('221', resp.toString('utf-8').trim().substr(0, 3)); 39 | test.done(); 40 | }); 41 | 42 | }, 43 | 'HELO': function(test) { 44 | var cmds = ['HELO FOO']; 45 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 46 | test.equal('250', resp.toString('utf-8').trim().substr(0, 3)); 47 | test.done(); 48 | }); 49 | 50 | }, 51 | 'HELO fails': function(test) { 52 | var cmds = ['HELO']; 53 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 54 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 55 | test.done(); 56 | }); 57 | }, 58 | 'EHLO': function(test) { 59 | var cmds = ['EHLO FOO']; 60 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 61 | resp = resp.toString('utf-8').trim(); 62 | var lines = resp.split('\r\n'); 63 | for (var i = 0; i < lines.length - 1; i++) { 64 | test.equal('250-', lines[i].substr(0, 4)); 65 | } 66 | test.equal('250 ', lines[i].substr(0, 4)); 67 | test.done(); 68 | }); 69 | }, 70 | 'EHLO fails': function(test) { 71 | var cmds = ['EHLO']; 72 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 73 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 74 | test.done(); 75 | }); 76 | }, 77 | 'HELO after STARTTLS': function(test) { 78 | var cmds = ['EHLO FOO', 'STARTTLS', 'HELO FOO']; 79 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 80 | test.equal('250', resp.toString('utf-8').trim().substr(0, 3)); 81 | test.done(); 82 | }); 83 | }, 84 | 'HELO fails after STARTTLS': function(test) { 85 | var cmds = ['EHLO FOO', 'STARTTLS', 'HELO']; 86 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 87 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 88 | test.done(); 89 | }); 90 | }, 91 | 'EHLO after STARTTLS': function(test) { 92 | var cmds = ['EHLO FOO', 'STARTTLS', 'HELO FOO']; 93 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 94 | resp = resp.toString('utf-8').trim(); 95 | var lines = resp.split('\r\n'); 96 | for (var i = 0; i < lines.length - 1; i++) { 97 | test.equal('250-', lines[i].substr(0, 4)); 98 | } 99 | test.equal('250 ', lines[i].substr(0, 4)); 100 | test.done(); 101 | }); 102 | }, 103 | 'EHLO fails after STARTTLS': function(test) { 104 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO']; 105 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 106 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 107 | test.done(); 108 | }); 109 | }, 110 | 'AUTH fails if not required': function(test) { 111 | var cmds = ['EHLO FOO', 'AUTH LOGIN']; 112 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 113 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 114 | test.done(); 115 | }); 116 | }, 117 | 'AUTH fails if not required TLS': function(test) { 118 | var cmds = ['EHLO FOO', 'STARTTLS', 'AUTH LOGIN']; 119 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 120 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 121 | test.done(); 122 | }); 123 | }, 124 | 'Custom Greeting banner': function(test) { 125 | var client = netlib.connect(PORT_NUMBER, function() { 126 | client.on('data', function(chunk) { 127 | test.equal('SCORPIO', (chunk || '').toString().trim().split(' ').pop()); 128 | client.end(); 129 | }); 130 | client.on('end', function() { 131 | test.done(); 132 | }); 133 | }); 134 | }, 135 | 'HELO name': function(test) { 136 | var cmds = ['HELO FOO']; 137 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 138 | test.equal('MYRDO', resp.toString('utf-8').trim().substr(4).split(' ').shift()); 139 | test.done(); 140 | }); 141 | }, 142 | 'EHLO name': function(test) { 143 | var cmds = ['EHLO FOO']; 144 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 145 | test.equal('MYRDO', resp.toString('utf-8').trim().substr(4).split(' ').shift()); 146 | test.done(); 147 | }); 148 | }, 149 | 'MAIL FROM options': function(test) { 150 | var cmds = ['HELO FOO', 'MAIL FROM: BODY=8BITMIME']; 151 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 152 | test.ok(resp.toString('utf-8').match(/^250/)); 153 | test.done(); 154 | }); 155 | }, 156 | 'MAXSIZE': function(test) { 157 | var cmds = ['EHLO FOO']; 158 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 159 | test.ok(resp.toString('utf-8').trim().match(/^250[\- ]SIZE 1234$/mi)); 160 | test.done(); 161 | }); 162 | } 163 | /*, 164 | 165 | // test disabled due to race conditions (returned false positives if connections are created in non expected order) 166 | 'Max Incoming Connections': function(test) { 167 | var maxClients = this.smtp.options.maxClients, 168 | name = this.smtp.options.name; 169 | 170 | for (var i = 0; i <= maxClients; i++) { 171 | runClientMockup(PORT_NUMBER, 'localhost', [], (function(i) { 172 | return function(resp) { 173 | if (i < maxClients) return; 174 | test.ok((new RegExp('^421\\s+' + name)).test(resp.toString('utf-8').trim())); 175 | test.done(); 176 | } 177 | })(i)); 178 | } 179 | }, 180 | */ 181 | }; 182 | 183 | exports['EHLO setting'] = { 184 | setUp: function(callback) { 185 | 186 | this.smtp = new simplesmtp.createServer({ 187 | disableEHLO: true 188 | }); 189 | this.smtp.listen(PORT_NUMBER, function(err) { 190 | if (err) { 191 | throw err; 192 | } else { 193 | callback(); 194 | } 195 | }); 196 | 197 | }, 198 | tearDown: function(callback) { 199 | this.smtp.end(callback); 200 | }, 201 | 'Disable EHLO': function(test) { 202 | runClientMockup(PORT_NUMBER, 'localhost', ['EHLO foo'], function(resp) { 203 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 204 | runClientMockup(PORT_NUMBER, 'localhost', ['HELO foo'], function(resp) { 205 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 206 | test.done(); 207 | }); 208 | }); 209 | 210 | } 211 | }; 212 | 213 | exports['Client disconnect'] = { 214 | 215 | 'Client disconnect': function(test) { 216 | 217 | var smtp = new simplesmtp.createServer(), 218 | clientEnvelope; 219 | smtp.listen(PORT_NUMBER, function(err) { 220 | if (err) { 221 | throw err; 222 | } 223 | 224 | runClientMockup(PORT_NUMBER, 'localhost', ['EHLO foo', 'MAIL FROM:', 'RCPT TO:', 'DATA'], function(resp) { 225 | test.equal('3', resp.toString('utf-8').trim().substr(0, 1)); 226 | }); 227 | 228 | }); 229 | smtp.on('startData', function(envelope) { 230 | clientEnvelope = envelope; 231 | }); 232 | smtp.on('close', function(envelope) { 233 | test.equal(envelope, clientEnvelope); 234 | smtp.end(function() {}); 235 | test.done(); 236 | }); 237 | 238 | } 239 | }; 240 | 241 | exports['Require AUTH'] = { 242 | setUp: function(callback) { 243 | 244 | this.smtp = new simplesmtp.createServer({ 245 | requireAuthentication: true, 246 | authMethods: ['PLAIN', 'LOGIN', 'XOAUTH2'] 247 | }); 248 | this.smtp.listen(PORT_NUMBER, function(err) { 249 | if (err) { 250 | throw err; 251 | } else { 252 | callback(); 253 | } 254 | }); 255 | 256 | this.smtp.on('authorizeUser', function(envelope, username, password, callback) { 257 | callback(null, username == 'andris' && password == 'test'); 258 | }); 259 | 260 | }, 261 | tearDown: function(callback) { 262 | this.smtp.end(callback); 263 | }, 264 | 'Fail without AUTH': function(test) { 265 | var cmds = ['EHLO FOO', 'MAIL FROM:']; 266 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 267 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 268 | test.done(); 269 | }); 270 | }, 271 | 'Unknown AUTH': function(test) { 272 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH CRAM']; 273 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 274 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 275 | test.done(); 276 | }); 277 | }, 278 | 'AUTH fails before STARTTLS': function(test) { 279 | var cmds = ['EHLO FOO', 'AUTH LOGIN']; 280 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 281 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 282 | test.done(); 283 | }); 284 | }, 285 | 'AUTH LOGIN': function(test) { 286 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN']; 287 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 288 | test.equal('3', resp.toString('utf-8').trim().substr(0, 1)); 289 | test.done(); 290 | }); 291 | }, 292 | 'AUTH LOGIN Invalid login': function(test) { 293 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 294 | new Buffer('inv').toString('base64'), 295 | new Buffer('alid').toString('base64') 296 | ]; 297 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 298 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 299 | test.done(); 300 | }); 301 | }, 302 | 'AUTH LOGIN Invalid username': function(test) { 303 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 304 | new Buffer('inv').toString('base64'), 305 | new Buffer('test').toString('base64') 306 | ]; 307 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 308 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 309 | test.done(); 310 | }); 311 | }, 312 | 'AUTH LOGIN Invalid password': function(test) { 313 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 314 | new Buffer('andris').toString('base64'), 315 | new Buffer('alid').toString('base64') 316 | ]; 317 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 318 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 319 | test.done(); 320 | }); 321 | }, 322 | 'AUTH LOGIN Login success': function(test) { 323 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 324 | new Buffer('andris').toString('base64'), 325 | new Buffer('test').toString('base64') 326 | ]; 327 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 328 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 329 | test.done(); 330 | }); 331 | }, 332 | 'AUTH LOGIN Login with username': function(test) { 333 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 334 | 'AUTH LOGIN ' + new Buffer('andris').toString('base64'), 335 | new Buffer('test').toString('base64') 336 | ]; 337 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 338 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 339 | test.done(); 340 | }); 341 | }, 342 | 'AUTH LOGIN Login with username - invalid username': function(test) { 343 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 344 | 'AUTH LOGIN ' + new Buffer('inv').toString('base64'), 345 | new Buffer('test').toString('base64') 346 | ]; 347 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 348 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 349 | test.done(); 350 | }); 351 | }, 352 | 'AUTH LOGIN Login with username - invalid password': function(test) { 353 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 354 | 'AUTH LOGIN ' + new Buffer('andris').toString('base64'), 355 | new Buffer('inv').toString('base64') 356 | ]; 357 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 358 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 359 | test.done(); 360 | }); 361 | }, 362 | 'AUTH PLAIN': function(test) { 363 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN']; 364 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 365 | test.equal('3', resp.toString('utf-8').trim().substr(0, 1)); 366 | test.done(); 367 | }); 368 | }, 369 | 'AUTH PLAIN Invalid login': function(test) { 370 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 371 | new Buffer('inv\u0000inv\u0000alid').toString('base64') 372 | ]; 373 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 374 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 375 | test.done(); 376 | }); 377 | }, 378 | 'AUTH PLAIN Invalid user': function(test) { 379 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 380 | new Buffer('inv\u0000inv\u0000test').toString('base64') 381 | ]; 382 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 383 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 384 | test.done(); 385 | }); 386 | }, 387 | 'AUTH PLAIN Invalid password': function(test) { 388 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 389 | new Buffer('andris\u0000andris\u0000alid').toString('base64') 390 | ]; 391 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 392 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 393 | test.done(); 394 | }); 395 | }, 396 | 'AUTH PLAIN Login success': function(test) { 397 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 398 | new Buffer('andris\u0000andris\u0000test').toString('base64') 399 | ]; 400 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 401 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 402 | test.done(); 403 | }); 404 | }, 405 | 'AUTH PLAIN Yet another login success': function(test) { 406 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN', 407 | new Buffer('andris\u0000andris\u0000test').toString('base64') 408 | ]; 409 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 410 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 411 | test.done(); 412 | }); 413 | }, 414 | 'AUTH XOAUTH2 Login success': function(test) { 415 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH XOAUTH2 ' + 416 | new Buffer([ 417 | 'user=andris', 418 | 'auth=Bearer test', 419 | '', 420 | '\n' 421 | ].join('\x01')).toString('base64') 422 | ]; 423 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 424 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 425 | test.done(); 426 | }); 427 | }, 428 | 'AUTH XOAUTH2 Login fail': function(test) { 429 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH XOAUTH2 ' + 430 | new Buffer([ 431 | 'user=andris', 432 | 'auth=Bearer test2', 433 | '', 434 | '\n' 435 | ].join('\x01')).toString('base64'), 436 | '' 437 | ]; 438 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 439 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 440 | test.done(); 441 | }); 442 | } 443 | }; 444 | 445 | exports['Enable AUTH'] = { 446 | setUp: function(callback) { 447 | 448 | this.smtp = new simplesmtp.createServer({ 449 | enableAuthentication: true 450 | }); 451 | this.smtp.listen(PORT_NUMBER, function(err) { 452 | if (err) { 453 | throw err; 454 | } else { 455 | callback(); 456 | } 457 | }); 458 | 459 | this.smtp.on('authorizeUser', function(envelope, username, password, callback) { 460 | callback(null, username == 'andris' && password == 'test'); 461 | }); 462 | 463 | }, 464 | tearDown: function(callback) { 465 | this.smtp.end(callback); 466 | }, 467 | 'Pass without AUTH': function(test) { 468 | var cmds = ['EHLO FOO', 'MAIL FROM:']; 469 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 470 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 471 | test.done(); 472 | }); 473 | }, 474 | 'Unknown AUTH': function(test) { 475 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH CRAM']; 476 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 477 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 478 | test.done(); 479 | }); 480 | }, 481 | 'AUTH fails before STARTTLS': function(test) { 482 | var cmds = ['EHLO FOO', 'AUTH LOGIN']; 483 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 484 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 485 | test.done(); 486 | }); 487 | }, 488 | 'AUTH LOGIN': function(test) { 489 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN']; 490 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 491 | test.equal('3', resp.toString('utf-8').trim().substr(0, 1)); 492 | test.done(); 493 | }); 494 | }, 495 | 'AUTH LOGIN Invalid login': function(test) { 496 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 497 | new Buffer('inv').toString('base64'), 498 | new Buffer('alid').toString('base64') 499 | ]; 500 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 501 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 502 | test.done(); 503 | }); 504 | }, 505 | 'AUTH LOGIN Invalid username': function(test) { 506 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 507 | new Buffer('inv').toString('base64'), 508 | new Buffer('test').toString('base64') 509 | ]; 510 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 511 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 512 | test.done(); 513 | }); 514 | }, 515 | 'AUTH LOGIN Invalid password': function(test) { 516 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 517 | new Buffer('andris').toString('base64'), 518 | new Buffer('alid').toString('base64') 519 | ]; 520 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 521 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 522 | test.done(); 523 | }); 524 | }, 525 | 'AUTH LOGIN Login success': function(test) { 526 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH LOGIN', 527 | new Buffer('andris').toString('base64'), 528 | new Buffer('test').toString('base64') 529 | ]; 530 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 531 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 532 | test.done(); 533 | }); 534 | }, 535 | 'AUTH PLAIN': function(test) { 536 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN']; 537 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 538 | test.equal('3', resp.toString('utf-8').trim().substr(0, 1)); 539 | test.done(); 540 | }); 541 | }, 542 | 'AUTH PLAIN Invalid login': function(test) { 543 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 544 | new Buffer('inv\u0000inv\u0000alid').toString('base64') 545 | ]; 546 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 547 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 548 | test.done(); 549 | }); 550 | }, 551 | 'AUTH PLAIN Invalid user': function(test) { 552 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 553 | new Buffer('inv\u0000inv\u0000test').toString('base64') 554 | ]; 555 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 556 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 557 | test.done(); 558 | }); 559 | }, 560 | 'AUTH PLAIN Invalid password': function(test) { 561 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 562 | new Buffer('andris\u0000andris\u0000alid').toString('base64') 563 | ]; 564 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 565 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 566 | test.done(); 567 | }); 568 | }, 569 | 'AUTH PLAIN Login success': function(test) { 570 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN ' + 571 | new Buffer('andris\u0000andris\u0000test').toString('base64') 572 | ]; 573 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 574 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 575 | test.done(); 576 | }); 577 | }, 578 | 'AUTH PLAIN Yet another login success': function(test) { 579 | var cmds = ['EHLO FOO', 'STARTTLS', 'EHLO FOO', 'AUTH PLAIN', 580 | new Buffer('andris\u0000andris\u0000test').toString('base64') 581 | ]; 582 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 583 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 584 | test.done(); 585 | }); 586 | } 587 | }; 588 | 589 | exports.ignoreTLS = { 590 | setUp: function(callback) { 591 | 592 | this.smtp = new simplesmtp.createServer({ 593 | requireAuthentication: true, 594 | ignoreTLS: true 595 | }); 596 | this.smtp.listen(PORT_NUMBER, function(err) { 597 | if (err) { 598 | throw err; 599 | } else { 600 | callback(); 601 | } 602 | }); 603 | 604 | this.smtp.on('authorizeUser', function(envelope, username, password, callback) { 605 | callback(null, username == 'd3ph' && password == 'test'); 606 | }); 607 | }, 608 | tearDown: function(callback) { 609 | this.smtp.end(callback); 610 | }, 611 | 'Fail without AUTH': function(test) { 612 | var cmds = ['EHLO FOO', 'MAIL FROM:']; 613 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 614 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 615 | test.done(); 616 | }); 617 | }, 618 | 'Fail MAIL FROM without HELO': function(test) { 619 | var cmds = ['MAIL FROM:']; 620 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 621 | test.equal('5', resp.toString('utf-8').trim().substr(0, 1)); 622 | test.done(); 623 | }); 624 | }, 625 | 'Success AUTH & SEND MAIL with .': function(test) { 626 | var cmds = ['EHLO FOO', 627 | 'AUTH PLAIN', 628 | new Buffer('\u0000d3ph\u0000test').toString('base64'), 629 | 'MAIL FROM:', 630 | 'RCPT TO:', 631 | 'DATA', 632 | 'Test mail\r\n.\r\n', 633 | ]; 634 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 635 | resp = resp.toString('utf-8').trim(); 636 | test.equal('2', resp.substr(0, 1)); 637 | test.ok(resp.match('queued as')); 638 | test.done(); 639 | }); 640 | } 641 | }; 642 | 643 | exports['Sending mail listen for dataReady'] = { 644 | setUp: function(callback) { 645 | var data = ''; 646 | 647 | this.smtp = new simplesmtp.createServer({ 648 | ignoreTLS: true 649 | }); 650 | this.smtp.listen(PORT_NUMBER, function(err) { 651 | if (err) { 652 | throw err; 653 | } else { 654 | callback(); 655 | } 656 | }); 657 | 658 | this.smtp.on('authorizeUser', function(envelope, username, password, callback) { 659 | callback(null, username == 'd3ph' && password == 'test'); 660 | }); 661 | 662 | this.smtp.on('data', function(envelope, chunk) { 663 | data += chunk; 664 | }); 665 | 666 | this.smtp.on('dataReady', function(envelope, callback) { 667 | setTimeout(function() { 668 | if (data.match('spam')) { 669 | callback(new Error('FAILED')); 670 | } else { 671 | callback(null, '#ID'); 672 | } 673 | }, 2000); 674 | }); 675 | }, 676 | tearDown: function(callback) { 677 | this.smtp.end(callback); 678 | }, 679 | 'Fail send mail if body contains "spam"': function(test) { 680 | var cmds = ['EHLO FOO', 681 | 'MAIL FROM:', 682 | 'RCPT TO:', 683 | 'DATA', 684 | 'Test mail with spam!\r\n.\r\n', 685 | ]; 686 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 687 | test.equal('550 FAILED', resp.toString('utf-8').trim()); 688 | test.done(); 689 | }); 690 | }, 691 | 'Create #ID for mail': function(test) { 692 | var cmds = ['EHLO FOO', 693 | 'MAIL FROM:', 694 | 'RCPT TO:', 695 | 'DATA', 696 | 'Clear mail body\r\n.\r\n', 697 | ]; 698 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 699 | resp = resp.toString('utf-8').trim(); 700 | test.equal('2', resp.substr(0, 1)); 701 | test.ok(resp.match('#ID')); 702 | test.done(); 703 | }); 704 | } 705 | }; 706 | 707 | exports['Sending mail listen for dataReady'] = { 708 | setUp: function(callback) { 709 | var data = ''; 710 | 711 | this.smtp = new simplesmtp.createServer({ 712 | ignoreTLS: true, 713 | disableDNSValidation: true 714 | }); 715 | this.smtp.listen(PORT_NUMBER, function(err) { 716 | if (err) { 717 | throw err; 718 | } else { 719 | callback(); 720 | } 721 | }); 722 | 723 | this.smtp.on('data', function(envelope, chunk) { 724 | data += chunk; 725 | }); 726 | }, 727 | 728 | tearDown: function(callback) { 729 | this.smtp.end(callback); 730 | }, 731 | 732 | 'Allow empty Mail from': function(test) { 733 | var cmds = ['EHLO FOO', 'MAIL FROM:<>']; 734 | runClientMockup(PORT_NUMBER, 'localhost', cmds, function(resp) { 735 | test.equal('2', resp.toString('utf-8').trim().substr(0, 1)); 736 | test.done(); 737 | }); 738 | } 739 | }; -------------------------------------------------------------------------------- /test/testmessage.eml: -------------------------------------------------------------------------------- 1 | From: test@pangalink.net 2 | To: test@pangalink.net 3 | Subject: Test 4 | 5 | Hello world! --------------------------------------------------------------------------------