Skip to content

Commit

Permalink
Switch to modern nodemailer 4, Node 4 version. Fix #8591 (#8605)
Browse files Browse the repository at this point in the history
* Switch to modern nodemailer 4, Node 4 version. Fix #8591

* Most critically, use a pool instead of direct SMTP connection,
  to handle dropped connections and increase throughput,
  like mail module 1.1.  (#8591)
* New nodemailer's sendMail wants an options object, not a MailComposer
  object.  Luckily, a MailComposer object has a "mail" field that
  remembers the original options, so we can keep original behavior.
* However, we no longer support the mailComposer option set to a compiled
  MailComposer object (functionality that was briefly added in 1.2.0).
* nodemailer does SMTP URL parsing now automatically for us, simplifying code.
* Tests' outputs now end with additional "\r\n"
* Drop underscore package dependency (no longer needed)

* General formatting/style cleanup for `packages/email`.

* snake_cased => camelCased for some local variables.
* Added curly-brackets to `if`s.
* Removed trailing spaces.
* Removed commented-out code.
* Removed older doc text and changed some links.

* Get rid of back-and-forth assigning of `mailUrlString`.
  • Loading branch information
edemaine authored and abernix committed Apr 25, 2017
1 parent d5ba2a1 commit beb6009
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 198 deletions.
73 changes: 4 additions & 69 deletions packages/email/.npm/package/npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

135 changes: 48 additions & 87 deletions packages/email/email.js
@@ -1,96 +1,95 @@
var Future = Npm.require('fibers/future');
var urlModule = Npm.require('url');
var SMTPConnection = Npm.require('smtp-connection');
var nodemailer = Npm.require('node4mailer');

Email = {};
EmailTest = {};

EmailInternals = {
NpmModules: {
mailcomposer: {
version: Npm.require('mailcomposer/package.json').version,
module: Npm.require('mailcomposer')
version: Npm.require('node4mailer/package.json').version,
module: Npm.require('node4mailer/lib/mail-composer')
},
nodemailer: {
version: Npm.require('node4mailer/package.json').version,
module: Npm.require('node4mailer')
}
}
};

var mailcomposer = EmailInternals.NpmModules.mailcomposer.module;
var MailComposer = EmailInternals.NpmModules.mailcomposer.module;

var makePool = function (mailUrlString) {
var mailUrl = urlModule.parse(mailUrlString);
if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:')
var makeTransport = function (mailUrlString) {
var mailUrl = urlModule.parse(mailUrlString, true);

if (mailUrl.protocol !== 'smtp:' && mailUrl.protocol !== 'smtps:') {
throw new Error("Email protocol in $MAIL_URL (" +
mailUrlString + ") must be 'smtp' or 'smtps'");
}

var port = +(mailUrl.port);
var auth = false;
if (mailUrl.auth) {
var parts = mailUrl.auth.split(':', 2);
auth = {user: parts[0],
pass: parts[1]};
// Allow overriding pool setting, but default to true.
if (!mailUrl.query) {
mailUrl.query = {};
}

var pool = new SMTPConnection({
port: port, // Defaults to 25
host: mailUrl.hostname, // Defaults to "localhost"
secure: (port === 465) || (mailUrl.protocol === 'smtps:')
});
Meteor.wrapAsync(pool.connect, pool)();
if (auth) {
//_.bind(Future.wrap(pool.login), pool)(auth).wait();
Meteor.wrapAsync(pool.login, pool)(auth);
if (!mailUrl.query.pool) {
mailUrl.query.pool = 'true';
}

pool._syncSend = Meteor.wrapAsync(pool.send, pool);
return pool;
var transport = nodemailer.createTransport(
urlModule.format(mailUrl));

transport._syncSendMail = Meteor.wrapAsync(transport.sendMail, transport);
return transport;
};

var getPool = function() {
var getTransport = function() {
// We delay this check until the first call to Email.send, in case someone
// set process.env.MAIL_URL in startup code. Then we store in a cache until
// process.env.MAIL_URL changes.
var url = process.env.MAIL_URL;
if (this.cacheKey === undefined || this.cacheKey !== url) {
this.cacheKey = url;
this.cache = url ? makePool(url) : null;
this.cache = url ? makeTransport(url) : null;
}
return this.cache;
}

var next_devmode_mail_id = 0;
var nextDevModeMailId = 0;
var output_stream = process.stdout;

// Testing hooks
EmailTest.overrideOutputStream = function (stream) {
next_devmode_mail_id = 0;
nextDevModeMailId = 0;
output_stream = stream;
};

EmailTest.restoreOutputStream = function () {
output_stream = process.stdout;
};

var devModeSend = function (mc) {
var devmode_mail_id = next_devmode_mail_id++;
var devModeSend = function (mail) {
var devModeMailId = nextDevModeMailId++;

var stream = output_stream;

// This approach does not prevent other writers to stdout from interleaving.
stream.write("====== BEGIN MAIL #" + devmode_mail_id + " ======\n");
stream.write("====== BEGIN MAIL #" + devModeMailId + " ======\n");
stream.write("(Mail not sent; to enable sending, set the MAIL_URL " +
"environment variable.)\n");
var readStream = mc.createReadStream();
var readStream = new MailComposer(mail).compile().createReadStream();
readStream.pipe(stream, {end: false});
var future = new Future;
readStream.on('end', function () {
stream.write("====== END MAIL #" + devmode_mail_id + " ======\n");
stream.write("====== END MAIL #" + devModeMailId + " ======\n");
future.return();
});
future.wait();
};

var smtpSend = function (pool, mc) {
pool._syncSend(mc.getEnvelope(), mc.createReadStream());
var smtpSend = function (transport, mail) {
transport._syncSendMail(mail);
};

/**
Expand All @@ -105,28 +104,6 @@ EmailTest.hookSend = function (f) {
sendHooks.push(f);
};

// Old comment below
/**
* Send an email.
*
* Connects to the mail server configured via the MAIL_URL environment
* variable. If unset, prints formatted message to stdout. The "from" option
* is required, and at least one of "to", "cc", and "bcc" must be provided;
* all other options are optional.
*
* @param options
* @param options.from {String} RFC5322 "From:" address
* @param options.to {String|String[]} RFC5322 "To:" address[es]
* @param options.cc {String|String[]} RFC5322 "Cc:" address[es]
* @param options.bcc {String|String[]} RFC5322 "Bcc:" address[es]
* @param options.replyTo {String|String[]} RFC5322 "Reply-To:" address[es]
* @param options.subject {String} RFC5322 "Subject:" line
* @param options.text {String} RFC5322 mail body (plain text)
* @param options.html {String} RFC5322 mail body (HTML)
* @param options.headers {Object} custom RFC5322 headers (dictionary)
*/

// New API doc comment below
/**
* @summary Send an email. Throws an `Error` on failure to contact mail server
* or if mail server returns an error. All fields should match
Expand All @@ -135,10 +112,9 @@ EmailTest.hookSend = function (f) {
* If the `MAIL_URL` environment variable is set, actually sends the email.
* Otherwise, prints the contents of the email to standard out.
*
* Note that this package is based on mailcomposer version `4.0.1`, so make
* sure to refer to the documentation for that version if using the
* `attachments` or `mailComposer` options.
* [Click here to read the mailcomposer 4.0.1 docs](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md).
* Note that this package is based on **mailcomposer 4**, so make sure to refer to
* [the documentation](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md)
* for that version when using the `attachments` or `mailComposer` options.
*
* @locus Server
* @param {Object} options
Expand All @@ -150,44 +126,29 @@ EmailTest.hookSend = function (f) {
* @param {String} [options.messageId] Message-ID for this message; otherwise, will be set to a random value
* @param {String} [options.subject] "Subject:" line
* @param {String} [options.text|html] Mail body (in plain text and/or HTML)
* @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch
* @param {String} [options.icalEvent] iCalendar event attachment
* @param {String} [options.watchHtml] Mail body in HTML specific for Apple Watch
* @param {String} [options.icalEvent] iCalendar event attachment
* @param {Object} [options.headers] Dictionary of custom headers
* @param {Object[]} [options.attachments] Array of attachment objects, as
* described in the [mailcomposer documentation](https://github.com/nodemailer/mailcomposer/blob/v4.0.1/README.md#attachments).
* @param {MailComposer} [options.mailComposer] A [MailComposer](https://github.com/andris9/mailcomposer)
* object (or its `compile()` output) representing the message to be sent.
* Overrides all other options. You can access the `mailcomposer` npm module at
* `EmailInternals.NpmModules.mailcomposer.module`. This module is a function
* which assembles a MailComposer object and immediately `compile()`s it.
* Alternatively, you can create and pass a MailComposer object via
* `new EmailInternals.NpmModules.mailcomposer.module.MailComposer`.
* @param {MailComposer} [options.mailComposer] A [MailComposer](https://nodemailer.com/extras/mailcomposer/#e-mail-message-fields)
* object representing the message to be sent. Overrides all other options.
* You can create a `MailComposer` object via
* `new EmailInternals.NpmModules.mailcomposer.module`.
*/
Email.send = function (options) {
for (var i = 0; i < sendHooks.length; i++)
if (! sendHooks[i](options))
return;

var mc;
if (options.mailComposer) {
mc = options.mailComposer;
if (mc.compile) {
mc = mc.compile();
}
} else {
// mailcomposer now automatically adds date if omitted
//if (!options.hasOwnProperty('date') &&
// (!options.headers || !options.headers.hasOwnProperty('Date'))) {
// options['date'] = new Date().toUTCString().replace(/GMT/, '+0000');
//}

mc = mailcomposer(options);
options = options.mailComposer.mail;
}

var pool = getPool();
if (pool) {
smtpSend(pool, mc);
var transport = getTransport();
if (transport) {
smtpSend(transport, options);
} else {
devModeSend(mc);
devModeSend(options);
}
};

0 comments on commit beb6009

Please sign in to comment.