├── .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 | [](http://travis-ci.org/andris9/simplesmtp) 18 | [](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 | *{user:'...', pass:'...'}
33 | * 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
{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
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
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
Handle response for a DATA
command
Handle response for a DATA
stream
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 | *{user:'...', pass:'...'}
22 | * 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} connectionsimplesmtp.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
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 | *['PLAIN', 'LOGIN']
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
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
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
Add recipients to the e-mail envelope
388 | * 389 | *This doesn't work if MAIL
command is not yet executed
If validateRecipients
option is set to true, then emits
392 | * 'validateRecipient'
and wait for the callback before moving
393 | * on
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
Switch to data mode and starts waiting for a binary data stream. Emits
498 | * 'startData'
.
If RCPT
is not yet run, stop
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.
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
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
Detect login information from the payload and initiate authentication
734 | * by emitting 'authorizeUser'
and waiting for its callback
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.