diff --git a/README.md b/README.md index be172a2..8f517c5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Email helper for KeystoneJS and Node.js Applications. Makes it easy to send dyna Features include: * Express-like template system (including support for any express-compatible template engine) -* Support for different email sending services (Mandrill and Mailgun are available now, more may be added) +* Support for different email sending services (Mandrill, Mailgun and AWS SES are available now, more may be added) * Understands Keystone User models, making it easy to send emails to the results of a query * Automatically transforms user variables into the correct format for each email service, for mail-merge style variable use * CSS Stylesheets are automatically inlined for robust email client compatibility @@ -101,6 +101,8 @@ See [the Mandrill API Docs](https://mandrillapp.com/api/docs/messages.nodejs.htm ## Usage with Nodemailer +Add a dependency for `nodemailer` to your project. + The following `send()` options are applicable when using `nodemailer` as the transport: - `from` (String or Object) The name and email to send from (see below) @@ -110,6 +112,24 @@ The following `send()` options are applicable when using `nodemailer` as the tra See [the Nodemailer README](https://github.com/nodemailer/nodemailer) for more information about supported transports and plugins. +## Usage with AWS SES + +Add a dependency for `aws-sdk`, i.e. add `"aws-sdk": "^2.20.0",` to package.json of your project. + +The following `send()` options are applicable when using `awsses` as the transport: + +- `apiKey` (required, String) Your API Key, defaults to `process.env.AWS_SES_API_KEY` +- `apiSecret` (required, String) Your Secret Key, defaults to `process.env.AWS_SES_SECRET_KEY` +- `region` (required, String) Your Sending Region i.e., us-east-1, defaults to `process.env.AWS_SES_REGION` +- `from` (String or Object) The name and email to send from (see below) +- `inline_css` (Boolean) inline CSS classes in your template, defaults to `true` +- `to` (String, Object or Array) The recipient(s) of the email (see below) +- `cc` (String, Object or Array) The CC recipient(s) of the email (same format as `to`, see below) +- `bcc` (String, Object or Array) The BCC recipient(s) of the email (same format as `to`, see below) +- `replyTo` (String, Object or Array) The Reply-To address(es) of the email (same format as `to`, see below) + +See [the AWS SES API Docs](http://docs.aws.amazon.com/ses/latest/APIReference/API_SendEmail.html) for the full set of supported options. + ## From option The `from` option can be a String (email address), or Object containing `name` and `email`. In the object form, `name` can also be an object containing `first` and `last` (which will be concatenated with a space). This simplifies usage with `User` models in KeystoneJS. For example: @@ -181,6 +201,10 @@ You could previously use a directory as the template name, and Keystone.Email wo Keystone would previous return errors if the subject, contents, recipient(s) or the sender address were invalid. This is no longer handled by the library, please make sure you validate these options before sending the emails. If the transport validates these options, errors will be passed directly to the callback. +### AWS SES limitations + +AWS SES does not support click tracking + ## License MIT Licensed. Copyright (c) 2016 Jed Watson. diff --git a/lib/Email.js b/lib/Email.js index 614bb03..d34cb6f 100644 --- a/lib/Email.js +++ b/lib/Email.js @@ -115,7 +115,7 @@ Email.prototype.send = function (renderOptions, sendOptions, callback) { if (typeof sendOptions === 'function') { throw new Error('[Email.send] Please pass renderOptions, sendOptions and a callback. See the documentation for more information!'); } else { - // callback is optional, we default it to a no-op so because itmakes it + // callback is optional, we default it to a no-op so because it makes it // easier to always expect a valid function in the transport implementations callback = function () {}; } diff --git a/lib/transports/awsses/getRecipients.js b/lib/transports/awsses/getRecipients.js new file mode 100644 index 0000000..e049d03 --- /dev/null +++ b/lib/transports/awsses/getRecipients.js @@ -0,0 +1,16 @@ +var assign = require('object-assign'); + +var processAddress = require('../../util/processAddress'); + +function getRecipients(to) { + var recipients = []; + (Array.isArray(to) ? to : [to]).forEach(function (i) { + var rcpt = processAddress(i); + if (!rcpt.email) return; + recipients.push(rcpt.email); + }); + + return recipients; +}; + +module.exports = getRecipients; diff --git a/lib/transports/awsses/getSendOptions.js b/lib/transports/awsses/getSendOptions.js new file mode 100644 index 0000000..8f21a70 --- /dev/null +++ b/lib/transports/awsses/getSendOptions.js @@ -0,0 +1,35 @@ +var assign = require('object-assign'); + +var getRecipients = require('./getRecipients'); +var processAddress = require('../../util/processAddress'); + +var defaultOptions = { + apiKey: process.env.AWS_SES_API_KEY, + apiSecret: process.env.AWS_SES_SECRET_KEY, + region: process.env.AWS_SES_REGION || 'us-east-1', +}; + +function getSendOptions(options) { + // default options + options = assign({ Destination: {} }, defaultOptions, options); + // process from name and email + options.Source = processAddress(options.from).address; + // process recipients, including cc and bcc + options.Destination.ToAddresses = getRecipients(options.to); + // handle cc + if (options.cc && options.cc.length > 0) { + options.Destination.CcAddresses = getRecipients(options.cc); + } + // handle bcc + if (options.bcc && options.bcc.length > 0) { + options.Destination.BccAddresses = getRecipients(options.bcc); + } + // handle reply-to + if (options.replyTo && options.replyTo.length > 0) { + options.ReplyToAddresses = getRecipients(options.replyTo); + } + // return + return options; +} + +module.exports = getSendOptions; diff --git a/lib/transports/awsses/index.js b/lib/transports/awsses/index.js new file mode 100644 index 0000000..823e49c --- /dev/null +++ b/lib/transports/awsses/index.js @@ -0,0 +1,47 @@ +var requireOptional = require('../../util/requireOptional'); +var assign = require('object-assign'); +var aws = requireOptional('aws-sdk', 'Please install the aws-sdk package to use this transport'); +var juice = require('juice'); +var getSendOptions = require('./getSendOptions'); + +function send(email, options, callback) { + // init options + options = assign({ Message: { Subject: {}, Body: { Html: {} } } }, getSendOptions(options), email); + + // validate + if (!options.Destination.ToAddresses.length) { + return callback(new Error('No recipients to send to')); + } + + // handle html body + options.Message.Body.Html.Data = options.inlineCSS ? juice(options.html) : options.html; + + // add subject + if (options.subject) { + options.Message.Subject.Data = options.subject; + } + + // init ses + var ses = new aws.SES({ + apiVersion: '2010-12-01', + accessKeyId: options.apiKey, + secretAccessKey: options.apiSecret, + region: options.region + }); + + var params = { + Message: options.Message, + Destination: options.Destination, + Source: options.Source, + }; + + // add reply-to + if (options.ReplyToAddresses) { + params.ReplyToAddresses = options.ReplyToAddresses + } + + // send emails + ses.sendEmail(params, callback); +} + +module.exports = send; diff --git a/lib/transports/nodemailer/index.js b/lib/transports/nodemailer/index.js index 916557b..8b7908c 100644 --- a/lib/transports/nodemailer/index.js +++ b/lib/transports/nodemailer/index.js @@ -1,4 +1,4 @@ -var requireOptional = require('../util/requireOptional'); +var requireOptional = require('../../util/requireOptional'); var assign = require('object-assign'); var nodemailer = requireOptional('nodemailer', 'Please install the nodemailer package to use this transport'); diff --git a/lib/util/requireOptional.js b/lib/util/requireOptional.js index de07cb4..8bb3852 100644 --- a/lib/util/requireOptional.js +++ b/lib/util/requireOptional.js @@ -1,6 +1,6 @@ module.exports = function (pkg, msg) { try { - return require('pkg'); + return require(pkg); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') { throw new Error(msg); diff --git a/package.json b/package.json index 0a06c66..09a1d08 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "html-to-text": "^2.1.3", "juice": "^3.0.0", "lodash": "^4.15.0", - "mailgun-js": "^0.7.12", + "mailgun-js": "^0.22.0", "mandrill-api": "^1.0.45", "object-assign": "^4.1.0" }, @@ -37,8 +37,10 @@ "email", "keystone", "keystonejs", - "mandrill", - "mailgun" + "mailgun", + "ses", + "aws", + "mandrill" ], "author": "Jed Watson", "license": "MIT", diff --git a/tests/index.js b/tests/index.js index 32b58e0..1ff1ee0 100644 --- a/tests/index.js +++ b/tests/index.js @@ -51,6 +51,7 @@ describe('utils', function () { it('should return transport if transport is found', function () { var res1 = getTransport('mailgun'); var res2 = getTransport('mandrill'); + // Note: No tests are possible for 'nodemailer' or 'awsses' as they rely on optional dependencies assert.equal(typeof res1, 'function'); assert.equal(typeof res2, 'function'); });