From 9267a379832718b03371a28165a02b053f81ccb3 Mon Sep 17 00:00:00 2001 From: Dmitry Gorbash Date: Mon, 23 Oct 2017 21:25:59 +0300 Subject: [PATCH] Support for dynamic list of recipients --- index.js | 85 ++++++++++++++++++- test/promise.js | 49 +++++++++++ test/transformRecipients.js | 165 ++++++++++++++++++++++++++++++++++++ 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 test/promise.js diff --git a/index.js b/index.js index ead0495..6e532dc 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,10 @@ console.log("AWS Lambda SES Forwarder // @arithmetric // Version 4.2.0"); // - emailKeyPrefix: S3 key name prefix where SES stores email. Include the // trailing slash. // +// - skipRejectedRecipients: if TRUE - rejected Promises in forwardMapping +// will be silently ignored, otherwise entire list of recipients will be +// ignored (no messages will be sent). +// // - forwardMapping: Object where the key is the lowercase email address from // which to forward and the value is an array of email addresses to which to // send the message. @@ -32,6 +36,7 @@ var defaultConfig = { subjectPrefix: "", emailBucket: "s3-bucket-name", emailKeyPrefix: "emailsPrefix/", + skipRejectedRecipients: false, forwardMapping: { "info@example.com": [ "example.john@example.com", @@ -122,7 +127,27 @@ exports.transformRecipients = function(data) { } data.recipients = newRecipients; - return Promise.resolve(data); + let method = data.config.skipRejectedRecipients === true ? "settle" : "all"; + let promises = Promise.promisify(data.recipients, data); + return Promise[method](promises).then(recipients => { + // Flatten elements + return Array.prototype.concat.apply([], recipients); + }).then(recipients => { + // Remove empty elements + return recipients.filter(Boolean) + .map(String) + .map(s => String(s).trim()) + .filter(Boolean); + }).then(recipients => { + data.recipients = recipients; + return Promise.resolve(data); + }).catch(err => { + data.log({message: "Delivery cancelled. " + + "Original destinations: " + data.originalRecipients.join(", ") + ", " + + "Reason: " + err, level: "info"}); + data.recipients = []; + return Promise.resolve(data); + }); }; /** @@ -330,3 +355,61 @@ Promise.series = function(promises, initValue) { return chain.then(promise); }, Promise.resolve(initValue)); }; + +/** + * Wrap single item into Promise or list of items into list of Promises + * @param {*|*[]} item Item or list of items to wrap + * @param {*=} fnArgs - Additional arguments for functions + * @return {Promise[]|Promise} Promise or list of Promises + */ +Promise.promisify = function(item, ...fnArgs) { + if (Array.isArray(item)) { + // Array of values + return item.map(el => Promise.promisify(el, ...fnArgs)); + } else if (item === Object(item) && typeof item.then === 'function') { + // Promise + return item; + } else if (typeof item === 'function') { + // Function + return Promise.resolve(item.apply(null, fnArgs)); + } + // Resolve as-is + return Promise.resolve(item); +}; + +/** + * Waits for all promises to complete, even a rejected ones + * @param {Promise[]} promises List of promises + * @return {Promise} Promise which always fulfills + */ +Promise.settle = function(promises) { + let results = []; + let done = promises.length; + + return new Promise(resolve => { + // eslint-disable-next-line + function _resolve(i, v) { + results[i] = v; + done--; + if (done < 1) + resolve(results); + } + + // eslint-disable-next-line + function _reject(i, v) { + results[i] = undefined; + done--; + if (done < 1) + resolve(results); + } + + for (let i = 0; i < promises.length; i++) { + if (promises[i] && typeof promises[i].then === 'function') { + promises[i].then(_resolve.bind(null, i), _reject.bind(null, i)); + } else { + done--; + } + } + if (done < 1) resolve(results); + }); +}; diff --git a/test/promise.js b/test/promise.js new file mode 100644 index 0000000..8f35eb8 --- /dev/null +++ b/test/promise.js @@ -0,0 +1,49 @@ + +/* global describe, it */ + +let assert = require("assert"); + +require("../index"); + +describe('index.js', function() { + describe('#Promise.settle()', function() { + it('should fulfill on empty array', + function(done) { + let promises = []; + Promise.settle(promises) + .then(function(values) { + assert.equal(values.length, + 0, + "Promise.settle fulfills on empty array"); + done(); + }); + }); + + it('should fulfill on invalid array', + function(done) { + let promises = ['invalid']; + Promise.settle(promises) + .then(function(values) { + assert.equal(values.length, + 0, + "Promise.settle fulfills on invalid array"); + done(); + }); + }); + + it('should fulfill with rejected items', + function(done) { + let promises = [Promise.reject()]; + Promise.settle(promises) + .then(function(values) { + assert.strictEqual(values[0], + undefined, + "Promise.settle 1/2 fulfills with rejected items"); + assert.equal(values.length, + 1, + "Promise.settle 2/2 fulfills with rejected items"); + done(); + }); + }); + }); +}); diff --git a/test/transformRecipients.js b/test/transformRecipients.js index 2fabca9..712b772 100644 --- a/test/transformRecipients.js +++ b/test/transformRecipients.js @@ -113,6 +113,171 @@ describe('index.js', function() { }); }); + it('should accept Promises as recipient addresses', + function(done) { + var data = { + recipients: ["info@example.com"], + config: { + forwardMapping: { + "info@example.com": [ + Promise.resolve("jim@example.com"), + Promise.resolve("jane@example.com") + ] + } + }, + context: {}, + log: console.log + }; + index.transformRecipients(data) + .then(function(data) { + assert.equal(data.recipients[0], + "jim@example.com", + "parseEvent made 1/2 substitutions"); + assert.equal(data.recipients[1], + "jane@example.com", + "parseEvent made 2/2 substitutions"); + done(); + }); + }); + + it('should accept functions as recipient addresses', + function(done) { + var data = { + recipients: ["info@example.com"], + config: { + forwardMapping: { + "info@example.com": [ + () => "jim@example.com", + () => Promise.resolve("jane@example.com"), + () => { + return new Promise(function(resolve) { + setTimeout(() => { + resolve("bob@example.com"); + }, 10); + }); + } + ] + } + }, + context: {}, + log: console.log + }; + index.transformRecipients(data) + .then(function(data) { + assert.equal(data.recipients[0], + "jim@example.com", + "parseEvent made 1/3 substitutions"); + assert.equal(data.recipients[1], + "jane@example.com", + "parseEvent made 2/3 substitutions"); + assert.equal(data.recipients[2], + "bob@example.com", + "parseEvent made 3/3 substitutions"); + done(); + }); + }); + + it('functions should have access to data bundle', + function(done) { + var data = { + recipients: ["info@example.com"], + contextVar: "contextValue", + config: { + forwardMapping: { + "info@example.com": [ + data => data.contextVar + ] + } + }, + context: {}, + log: console.log + }; + index.transformRecipients(data) + .then(function(data) { + assert.equal(data.recipients[0], + "contextValue", + "parseEvent has correct context value"); + done(); + }); + }); + + it('should return no recipients (no skipRejectedRecipients in config)', + function(done) { + var data = { + recipients: ["info@example.com"], + config: { + forwardMapping: { + "info@example.com": [ + () => "jim@example.com", + () => "jane@example.com", + () => Promise.reject("Test rejection") + ] + } + }, + context: {}, + log: console.log + }; + index.transformRecipients(data) + .then(function(data) { + assert.equal(data.recipients.length, + 0, + "parseEvent recipients was rejected"); + done(); + }); + }); + + it('should return no recipients (skipRejectedRecipients=false)', + function(done) { + var data = { + recipients: ["info@example.com"], + config: { + skipRejectedRecipients: false, + forwardMapping: { + "info@example.com": [ + () => "jim@example.com", + () => "jane@example.com", + () => Promise.reject("Test rejection") + ] + } + }, + context: {}, + log: console.log + }; + index.transformRecipients(data) + .then(function(data) { + assert.equal(data.recipients.length, + 0, + "parseEvent recipients was rejected"); + done(); + }); + }); + + it('should skip rejected recipients (skipRejectedRecipients=true)', + function(done) { + var data = { + recipients: ["info@example.com"], + config: { + skipRejectedRecipients: true, + forwardMapping: { + "info@example.com": [ + () => "jim@example.com", + () => Promise.reject("Test rejection"), + () => "jane@example.com" + ] + } + }, + context: {}, + log: console.log + }; + index.transformRecipients(data) + .then(function(data) { + assert.equal(data.recipients.length, + 2, + "parseEvent rejected recipients was skipped"); + done(); + }); + }); + it('should exit if there are no new recipients', function(done) { var data = {