Skip to content

Support for dynamic list of recipients #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -32,6 +36,7 @@ var defaultConfig = {
subjectPrefix: "",
emailBucket: "s3-bucket-name",
emailKeyPrefix: "emailsPrefix/",
skipRejectedRecipients: false,
forwardMapping: {
"info@example.com": [
"example.john@example.com",
Expand Down Expand Up @@ -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);
});
};

/**
Expand Down Expand Up @@ -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);
});
};
49 changes: 49 additions & 0 deletions test/promise.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
165 changes: 165 additions & 0 deletions test/transformRecipients.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down