Comparing sensitive data, confidential files or internal emails?

Most legal and privacy policies prohibit uploading sensitive data online. Diffchecker Desktop ensures your confidential information never leaves your computer. Work offline and compare documents securely.

Index.js difference

Created Diff never expires
31 removals
397 lines
21 additions
387 lines
"use strict";
"use strict";


var {
const { S3Client, CopyObjectCommand, GetObjectCommand } = require("@aws-sdk/client-s3");
CopyObjectCommand,
const { SESClient, SendRawEmailCommand } = require("@aws-sdk/client-ses");
GetObjectCommand,
S3Client
} = require('@aws-sdk/client-s3');
var {
SendEmailCommand,
SESv2Client
} = require('@aws-sdk/client-sesv2');


console.log("AWS Lambda SES Forwarder // @arithmetric // Version 5.0.0");
console.log("AWS Lambda SES Forwarder // @arithmetric // Version 5.0.0");


// Configure the S3 bucket and key prefix for stored raw emails, and the
// Configure the S3 bucket and key prefix for stored raw emails, and the
// mapping of email addresses to forward from and to.
// mapping of email addresses to forward from and to.
//
//
// Expected keys/values:
// Expected keys/values:
//
//
// - fromEmail: Forwarded emails will come from this verified address
// - fromEmail: Forwarded emails will come from this verified address
//
//
// - subjectPrefix: Forwarded emails subject will contain this prefix
// - subjectPrefix: Forwarded emails subject will contain this prefix
//
//
// - emailBucket: S3 bucket name where SES stores emails.
// - emailBucket: S3 bucket name where SES stores emails.
//
//
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
// trailing slash.
// trailing slash.
//
//
// - allowPlusSign: Enables support for plus sign suffixes on email addresses.
// - allowPlusSign: Enables support for plus sign suffixes on email addresses.
// If set to `true`, the username/mailbox part of an email address is parsed
// If set to `true`, the username/mailbox part of an email address is parsed
// to remove anything after a plus sign. For example, an email sent to
// to remove anything after a plus sign. For example, an email sent to
// `example+test@example.com` would be treated as if it was sent to
// `example+test@example.com` would be treated as if it was sent to
// `example@example.com`.
// `example@example.com`.
//
//
// - forwardMapping: Object where the key is the lowercase email address from
// - 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
// which to forward and the value is an array of email addresses to which to
// send the message.
// send the message.
//
//
// To match all email addresses on a domain, use a key without the name part
// To match all email addresses on a domain, use a key without the name part
// of an email address before the "at" symbol (i.e. `@example.com`).
// of an email address before the "at" symbol (i.e. `@example.com`).
//
//
// To match a mailbox name on all domains, use a key without the "at" symbol
// To match a mailbox name on all domains, use a key without the "at" symbol
// and domain part of an email address (i.e. `info`).
// and domain part of an email address (i.e. `info`).
//
//
// To match all email addresses matching no other mapping, use "@" as a key.
// To match all email addresses matching no other mapping, use "@" as a key.
var defaultConfig = {
var defaultConfig = {
fromEmail: "noreply@example.com",
fromEmail: "noreply@example.com",
subjectPrefix: "",
subjectPrefix: "",
emailBucket: "s3-bucket-name",
emailBucket: "s3-bucket-name",
emailKeyPrefix: "emailsPrefix/",
emailKeyPrefix: "emailsPrefix/",
allowPlusSign: true,
allowPlusSign: true,
forwardMapping: {
forwardMapping: {
"info@example.com": [
"info@example.com": [
"example.john@example.com",
"example.john@example.com",
"example.jen@example.com"
"example.jen@example.com"
],
],
"abuse@example.com": [
"abuse@example.com": [
"example.jim@example.com"
"example.jim@example.com"
],
],
"@example.com": [
"@example.com": [
"example.john@example.com"
"example.john@example.com"
],
],
"info": [
"info": [
"info@example.com"
"info@example.com"
]
]
}
}
};
};


/**
/**
* Parses the SES event record provided for the `mail` and `receipients` data.
* Parses the SES event record provided for the `mail` and `receipients` data.
*
*
* @param {object} data - Data bundle with context, email, etc.
* @param {object} data - Data bundle with context, email, etc.
*
*
* @return {object} - Promise resolved with data.
* @return {object} - Promise resolved with data.
*/
*/
exports.parseEvent = function(data) {
exports.parseEvent = function(data) {
// Validate characteristics of a SES event record.
// Validate characteristics of a SES event record.
if (!data.event ||
if (!data.event ||
!data.event.hasOwnProperty('Records') ||
!data.event.hasOwnProperty('Records') ||
data.event.Records.length !== 1 ||
data.event.Records.length !== 1 ||
!data.event.Records[0].hasOwnProperty('eventSource') ||
!data.event.Records[0].hasOwnProperty('eventSource') ||
data.event.Records[0].eventSource !== 'aws:ses' ||
data.event.Records[0].eventSource !== 'aws:ses' ||
data.event.Records[0].eventVersion !== '1.0') {
data.event.Records[0].eventVersion !== '1.0') {
data.log({
data.log({
message: "parseEvent() received invalid SES message:",
message: "parseEvent() received invalid SES message:",
level: "error", event: JSON.stringify(data.event)
level: "error", event: JSON.stringify(data.event)
});
});
return Promise.reject(new Error('Error: Received invalid SES message.'));
return Promise.reject(new Error('Error: Received invalid SES message.'));
}
}


data.email = data.event.Records[0].ses.mail;
data.email = data.event.Records[0].ses.mail;
data.recipients = data.event.Records[0].ses.receipt.recipients;
data.recipients = data.event.Records[0].ses.receipt.recipients;
return Promise.resolve(data);
return Promise.resolve(data);
};
};


/**
/**
* Transforms the original recipients to the desired forwarded destinations.
* Transforms the original recipients to the desired forwarded destinations.
*
*
* @param {object} data - Data bundle with context, email, etc.
* @param {object} data - Data bundle with context, email, etc.
*
*
* @return {object} - Promise resolved with data.
* @return {object} - Promise resolved with data.
*/
*/
exports.transformRecipients = function(data) {
exports.transformRecipients = function(data) {
var newRecipients = [];
var newRecipients = [];
data.originalRecipients = data.recipients;
data.originalRecipients = data.recipients;
data.recipients.forEach(function(origEmail) {
data.recipients.forEach(function(origEmail) {
var origEmailKey = origEmail.toLowerCase();
var origEmailKey = origEmail.toLowerCase();
if (data.config.allowPlusSign) {
if (data.config.allowPlusSign) {
origEmailKey = origEmailKey.replace(/\+.*?@/, '@');
origEmailKey = origEmailKey.replace(/\+.*?@/, '@');
}
}
if (data.config.forwardMapping.hasOwnProperty(origEmailKey)) {
if (data.config.forwardMapping.hasOwnProperty(origEmailKey)) {
newRecipients = newRecipients.concat(
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailKey]);
data.config.forwardMapping[origEmailKey]);
data.originalRecipient = origEmail;
data.originalRecipient = origEmail;
} else {
} else {
var origEmailDomain;
var origEmailDomain;
var origEmailUser;
var origEmailUser;
var pos = origEmailKey.lastIndexOf("@");
var pos = origEmailKey.lastIndexOf("@");
if (pos === -1) {
if (pos === -1) {
origEmailUser = origEmailKey;
origEmailUser = origEmailKey;
} else {
} else {
origEmailDomain = origEmailKey.slice(pos);
origEmailDomain = origEmailKey.slice(pos);
origEmailUser = origEmailKey.slice(0, pos);
origEmailUser = origEmailKey.slice(0, pos);
}
}
if (origEmailDomain &&
if (origEmailDomain &&
data.config.forwardMapping.hasOwnProperty(origEmailDomain)) {
data.config.forwardMapping.hasOwnProperty(origEmailDomain)) {
newRecipients = newRecipients.concat(
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailDomain]);
data.config.forwardMapping[origEmailDomain]);
data.originalRecipient = origEmail;
data.originalRecipient = origEmail;
} else if (origEmailUser &&
} else if (origEmailUser &&
data.config.forwardMapping.hasOwnProperty(origEmailUser)) {
data.config.forwardMapping.hasOwnProperty(origEmailUser)) {
newRecipients = newRecipients.concat(
newRecipients = newRecipients.concat(
data.config.forwardMapping[origEmailUser]);
data.config.forwardMapping[origEmailUser]);
data.originalRecipient = origEmail;
data.originalRecipient = origEmail;
} else if (data.config.forwardMapping.hasOwnProperty("@")) {
} else if (data.config.forwardMapping.hasOwnProperty("@")) {
newRecipients = newRecipients.concat(
newRecipients = newRecipients.concat(
data.config.forwardMapping["@"]);
data.config.forwardMapping["@"]);
data.originalRecipient = origEmail;
data.originalRecipient = origEmail;
}
}
}
}
});
});


if (!newRecipients.length) {
if (!newRecipients.length) {
data.log({
data.log({
message: "Finishing process. No new recipients found for " +
message: "Finishing process. No new recipients found for " +
"original destinations: " + data.originalRecipients.join(", "),
"original destinations: " + data.originalRecipients.join(", "),
level: "info"
level: "info"
});
});
return data.callback();
return data.callback();
}
}


data.recipients = newRecipients;
data.recipients = newRecipients;
return Promise.resolve(data);
return Promise.resolve(data);
};
};


/**
/**
* Fetches the message data from S3.
* Fetches the message data from S3.
*
*
* @param {object} data - Data bundle with context, email, etc.
* @param {object} data - Data bundle with context, email, etc.
*
*
* @return {object} - Promise resolved with data.
* @return {object} - Promise resolved with data.
*/
*/
exports.fetchMessage = function(data) {
exports.fetchMessage = function(data) {
// Copying email object to ensure read permission
// Copying email object to ensure read permission
data.log({
data.log({
level: "info",
level: "info",
message: "Fetching email at s3://" + data.config.emailBucket + '/' +
message: "Fetching email at s3://" + data.config.emailBucket + '/' +
data.config.emailKeyPrefix + data.email.messageId
data.config.emailKeyPrefix + data.email.messageId
});
});
return new Promise(function(resolve, reject) {
return new Promise(function(resolve, reject) {
data.s3.send(new CopyObjectCommand({
data.s3.send(new CopyObjectCommand({
Bucket: data.config.emailBucket,
Bucket: data.config.emailBucket,
CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
data.email.messageId,
data.email.messageId,
Key: data.config.emailKeyPrefix + data.email.messageId,
Key: data.config.emailKeyPrefix + data.email.messageId,
ACL: 'private',
ACL: 'private',
ContentType: 'text/plain',
ContentType: 'text/plain',
StorageClass: 'STANDARD'
StorageClass: 'STANDARD'
}), function(err) {
}), function(err) {
if (err) {
if (err) {
data.log({
data.log({
level: "error",
level: "error",
message: "copyObject() returned error:",
message: "CopyObjectCommand() returned error:",
error: err,
error: err,
stack: err.stack
stack: err.stack
});
});
return reject(
return reject(
new Error("Error: Could not make readable copy of email."));
new Error("Error: Could not make readable copy of email."));
}
}


// Load the raw email from S3
// Load the raw email from S3
data.s3.send(new GetObjectCommand({
data.s3.send(new GetObjectCommand({
Bucket: data.config.emailBucket,
Bucket: data.config.emailBucket,
Key: data.config.emailKeyPrefix + data.email.messageId
Key: data.config.emailKeyPrefix + data.email.messageId
}), function(err, result) {
}), async function(err, result) {
if (err) {
if (err) {
data.log({
data.log({
level: "error",
level: "error",
message: "getObject() returned error:",
message: "GetObjectCommand() returned error:",
error: err,
error: err,
stack: err.stack
stack: err.stack
});
});
return reject(
return reject(
new Error("Error: Failed to load message body from S3."));
new Error("Error: Failed to load message body from S3."));
}
}
result.Body.transformToString().then(
data.emailData = await result.Body.transformToString();
body => {
return resolve(data);
data.emailData = body;
resolve(data);
}
);
});
});
});
});
});
});
};
};


/**
/**
* Processes the message data, making updates to recipients and other headers
* Processes the message data, making updates to recipients and other headers
* before forwarding message.
* before forwarding message.
*
*
* @param {object} data - Data bundle with context, email, etc.
* @param {object} data - Data bundle with context, email, etc.
*
*
* @return {object} - Promise resolved with data.
* @return {object} - Promise resolved with data.
*/
*/
exports.processMessage = function(data) {
exports.processMessage = function(data) {
var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
var header = match && match[1] ? match[1] : data.emailData;
var header = match && match[1] ? match[1] : data.emailData;
var body = match && match[2] ? match[2] : '';
var body = match && match[2] ? match[2] : '';


// Add "Reply-To:" with the "From" address if it doesn't already exists
// Add "Reply-To:" with the "From" address if it doesn't already exists
if (!/^reply-to:[\t ]?/mi.test(header)) {
if (!/^reply-to:[\t ]?/mi.test(header)) {
match = header.match(/^from:[\t ]?(.*(?:\r?\n\s+.*)*\r?\n)/mi);
match = header.match(/^from:[\t ]?(.*(?:\r?\n\s+.*)*\r?\n)/mi);
var from = match && match[1] ? match[1] : '';
var from = match && match[1] ? match[1] : '';
if (from) {
if (from) {
header = header + 'Reply-To: ' + from;
header = header + 'Reply-To: ' + from;
data.log({
data.log({
level: "info",
level: "info",
message: "Added Reply-To address of: " + from
message: "Added Reply-To address of: " + from
});
});
} else {
} else {
data.log({
data.log({
level: "info",
level: "info",
message: "Reply-To address not added because From address was not " +
message: "Reply-To address not added because From address was not " +
"properly extracted."
"properly extracted."
});
});
}
}
}
}


// SES does not allow sending messages from an unverified address,
// SES does not allow sending messages from an unverified address,
// so replace the message's "From:" header with the original
// so replace the message's "From:" header with the original
// recipient (which is a verified domain)
// recipient (which is a verified domain)
header = header.replace(
header = header.replace(
/^from:[\t ]?(.*(?:\r?\n\s+.*)*)/mgi,
/^from:[\t ]?(.*(?:\r?\n\s+.*)*)/mgi,
function(match, from) {
function(match, from) {
var fromText;
var fromText;
if (data.config.fromEmail) {
if (data.config.fromEmail) {
fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
' <' + data.config.fromEmail + '>';
' <' + data.config.fromEmail + '>';
} else {
} else {
fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
' <' + data.originalRecipient + '>';
' <' + data.originalRecipient + '>';
}
}
return fromText;
return fromText;
});
});


// Add a prefix to the Subject
// Add a prefix to the Subject
if (data.config.subjectPrefix) {
if (data.config.subjectPrefix) {
header = header.replace(
header = header.replace(
/^subject:[\t ]?(.*)/mgi,
/^subject:[\t ]?(.*)/mgi,
function(match, subject) {
function(match, subject) {
return 'Subject: ' + data.config.subjectPrefix + subject;
return 'Subject: ' + data.config.subjectPrefix + subject;
});
});
}
}


// Replace original 'To' header with a manually defined one
// Replace original 'To' header with a manually defined one
if (data.config.toEmail) {
if (data.config.toEmail) {
header = header.replace(/^to:[\t ]?(.*)/mgi, () => 'To: ' + data.config.toEmail);
header = header.replace(/^to:[\t ]?(.*)/mgi, () => 'To: ' + data.config.toEmail);
}
}


// Remove the Return-Path header.
// Remove the Return-Path header.
header = header.replace(/^return-path:[\t ]?(.*)\r?\n/mgi, '');
header = header.replace(/^return-path:[\t ]?(.*)\r?\n/mgi, '');


// Remove Sender header.
// Remove Sender header.
header = header.replace(/^sender:[\t ]?(.*)\r?\n/mgi, '');
header = header.replace(/^sender:[\t ]?(.*)\r?\n/mgi, '');


// Remove Message-ID header.
// Remove Message-ID header.
header = header.replace(/^message-id:[\t ]?(.*)\r?\n/mgi, '');
header = header.replace(/^message-id:[\t ]?(.*)\r?\n/mgi, '');


// Remove all DKIM-Signature headers to prevent triggering an
// Remove all DKIM-Signature headers to prevent triggering an
// "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
// "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
// These signatures will likely be invalid anyways, since the From
// These signatures will likely be invalid anyways, since the From
// header was modified.
// header was modified.
header = header.replace(/^dkim-signature:[\t ]?.*\r?\n(\s+.*\r?\n)*/mgi, '');
header = header.replace(/^dkim-signature:[\t ]?.*\r?\n(\s+.*\r?\n)*/mgi, '');


data.emailData = header + body;
data.emailData = header + body;
return Promise.resolve(data);
return Promise.resolve(data);
};
};


/**
/**
* Send email using the SES sendRawEmail command.
* Send email using the SES sendRawEmail command.
*
*
* @param {object} data - Data bundle with context, email, etc.
* @param {object} data - Data bundle with context, email, etc.
*
*
* @return {object} - Promise resolved with data.
* @return {object} - Promise resolved with data.
*/
*/
exports.sendMessage = function(data) {
exports.sendMessage = function(data) {
var params = {
Destinations: data.recipients,
Source: data.originalRecipient,
RawMessage: {
Data: Buffer.from(data.emailData)
}
};
data.log({
data.log({
level: "info",
level: "info",
message: "sendMessage: Sending email via SES. Original recipients: " +
message: "sendMessage: Sending email via SES. Original recipients: " +
data.originalRecipients.join(", ") + ". Transformed recipients: " +
data.originalRecipients.join(", ") + ". Transformed recipients: " +
data.recipients.join(", ") + "."
data.recipients.join(", ") + "."
});
});
return new Promise(function(resolve, reject) {
return new Promise(function(resolve, reject) {
data.ses.send(new SendEmailCommand({
data.ses.send(new SendRawEmailCommand(params), function(err, result) {
Content: {
Raw: {
Data: Buffer.from(data.emailData)
}
}
}), function(err, result) {
if (err) {
if (err) {
data.log({
data.log({
level: "error",
level: "error",
message: "sendRawEmail() returned error.",
message: "SendRawEmailCommand() returned error.",
error: err,
error: err,
stack: err.stack
stack: err.stack
});
});
return reject(new Error('Error: Email sending failed.'));
return reject(new Error('Error: Email sending failed.'));
}
}
data.log({
data.log({
level: "info",
level: "info",
message: "sendRawEmail() successful.",
message: "SendRawEmailCommand() successful.",
result: result
result: result
});
});
resolve(data);
resolve(data);
});
});
});
});
};
};


/**
/**
* Handler function to be invoked by AWS Lambda with an inbound SES email as
* Handler function to be invoked by AWS Lambda with an inbound SES email as
* the event.
* the event.
*
*
* @param {object} event - Lambda event from inbound email received by AWS SES.
* @param {object} event - Lambda event from inbound email received by AWS SES.
* @param {object} context - Lambda context object.
* @param {object} context - Lambda context object.
* @param {object} callback - Lambda callback object.
* @param {object} callback - Lambda callback object.
* @param {object} overrides - Overrides for the default data, including the
* @param {object} overrides - Overrides for the default data, including the
* configuration, SES object, and S3 object.
* configuration, SES object, and S3 object.
*/
*/
exports.handler = function(event, context, callback, overrides) {
exports.handler = function(event, context, callback, overrides) {
var steps = overrides && overrides.steps ? overrides.steps :
var steps = overrides && overrides.steps ? overrides.steps :
[
[
exports.parseEvent,
exports.parseEvent,
exports.transformRecipients,
exports.transformRecipients,
exports.fetchMessage,
exports.fetchMessage,
exports.processMessage,
exports.processMessage,
exports.sendMessage
exports.sendMessage
];
];
var data = {
var data = {
event: event,
event: event,
callback: callback,
callback: callback,
context: context,
context: context,
config: overrides && overrides.config ? overrides.config : defaultConfig,
config: overrides && overrides.config ? overrides.config : defaultConfig,
log: overrides && overrides.log ? overrides.log : console.log,
log: overrides && overrides.log ? overrides.log : console.log,
ses: overrides && overrides.ses ? overrides.ses : new SESv2Client(),
ses: overrides && overrides.ses ? overrides.ses : new SESClient(),
s3: overrides && overrides.s3 ?
s3: overrides && overrides.s3 ?
overrides.s3 : new S3Client({signatureVersion: 'v4'})
overrides.s3 : new S3Client({signatureVersion: 'v4'})
};
};
Promise.series(steps, data)
Promise.series(steps, data)
.then(function(data) {
.then(function(data) {
data.log({
data.log({
level: "info",
level: "info",
message: "Process finished successfully."
message: "Process finished successfully."
});
});
return data.callback();
return data.callback();
})
})
.catch(function(err) {
.catch(function(err) {
data.log({
data.log({
level: "error",
level: "error",
message: "Step returned error: " + err.message,
message: "Step returned error: " + err.message,
error: err,
error: err,
stack: err.stack
stack: err.stack
});
});
return data.callback(new Error("Error: Step returned error."));
return data.callback(new Error("Error: Step returned error."));
});
});
};
};


Promise.series = function(promises, initValue) {
Promise.series = function(promises, initValue) {
return promises.reduce(function(chain, promise) {
return promises.reduce(function(chain, promise) {
if (typeof promise !== 'function') {
if (typeof promise !== 'function') {
return chain.then(() => {
return chain.then(() => {
throw new Error("Error: Invalid promise item: " + promise);
throw new Error("Error: Invalid promise item: " + promise);
});
});
}
}
return chain.then(promise);
return chain.then(promise);
}, Promise.resolve(initValue));
}, Promise.resolve(initValue));
};
};