Skip to content

Instantly share code, notes, and snippets.

@iseebi iseebi/index.js
Last active Jul 3, 2019

Embed
What would you like to do?
"use strict";
console.log("AWS Lambda SES Forwarder - domain forward // modified by @iseebi, Rev.2 // Original: @arithmetric, Version 2.3.0 ");
// Configure the S3 bucket and key prefix for stored raw emails, and the
// mapping of email addresses to forward from and to.
//
// Expected keys/values:
// - fromEmail: Forwarded emails will come from this verified address
// - emailBucket: S3 bucket name where SES stores emails.
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
// trailing slash.
// - forwardSuffix: forward domain suffix
// - forwardToDomain: forward domain (maps: any@[account][forwardSuffix] -> [account]@[forwardToDomain])
var defaultConfig = {
fromEmail: "noreply@example.com",
emailBucket: "s3-bucket-name",
emailKeyPrefix: "emailsPrefix/",
forwardSuffix: ".mail.example.com",
forwardToDomain: "example.com"
};
var parseMailHeader = function(input) {
var returnValues = [];
for (var line of input.split(/\r?\n/)) {
var m = line.match(/^([^\s]+):(.*)$/);
if (m) {
returnValues.push({name: m[1], lines: [m[2]]});
}
else if ((returnValues.length > 0) && (line.length > 0)) {
returnValues[returnValues.length - 1].lines.push(line);
}
}
return returnValues;
};
var rebuildMailHeader = function(input) {
var returnValues = "";
for (var item of input) {
if (item.lines.length > 0) {
if (returnValues.length > 0) {
returnValues += "\r\n";
}
var name = item.name.replace(/\r\n/, '');
var value = item.lines.join("\r\n").replace(/(\r?\n){2,}/, "\r\n");
returnValues += name + ":" + value;
}
}
return returnValues;
}
/**
* Parses the SES event record provided for the `mail` and `receipients` data.
*
* @param {object} data - Data bundle with context, email, etc.
* @param {function} next - Callback function invoked as (error, data).
*/
exports.parseEvent = function(data, next) {
// Validate characteristics of a SES event record.
if (!data.event ||
!data.event.hasOwnProperty('Records') ||
data.event.Records.length !== 1 ||
!data.event.Records[0].hasOwnProperty('eventSource') ||
data.event.Records[0].eventSource !== 'aws:ses' ||
data.event.Records[0].eventVersion !== '1.0') {
data.log({message: "parseEvent() received invalid SES message:",
level: "error", event: JSON.stringify(data.event)});
data.context.fail('Error: Received invalid SES message.');
return;
}
data.email = data.event.Records[0].ses.mail;
data.recipients = data.event.Records[0].ses.receipt.recipients;
next(null, data);
};
/**
* Transforms the original recipients to the desired forwarded destinations.
*
* @param {object} data - Data bundle with context, email, etc.
* @param {function} next - Callback function invoked as (error, data).
*/
exports.transformRecipients = function(data, next) {
var newRecipients = [];
data.originalRecipients = data.recipients;
var re = new RegExp("@(.+)"+data.config.forwardSuffix.replace(".", "\\\.", "g")+"$");
data.recipients.forEach(function(origEmail) {
var matchResult = origEmail.match(re);
if (matchResult !== null) {
newRecipients = newRecipients.concat(
matchResult[1] + "@" + data.config.forwardToDomain);
data.originalRecipient = origEmail;
}
});
if (!newRecipients.length) {
data.log({message: "Finishing process. No new recipients found for " +
"original destinations: " + data.originalRecipients.join(", "),
level: "info"});
data.context.succeed();
return;
}
data.recipients = newRecipients;
next(null, data);
};
/**
* Fetches the message data from S3.
*
* @param {object} data - Data bundle with context, email, etc.
* @param {function} next - Callback function invoked as (error, data).
*/
exports.fetchMessage = function(data, next) {
// Copying email object to ensure read permission
data.log({level: "info", message: "Fetching email at s3://" +
data.config.emailBucket + '/' + data.config.emailKeyPrefix +
data.email.messageId});
data.s3.copyObject({
Bucket: data.config.emailBucket,
CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
data.email.messageId,
Key: data.config.emailKeyPrefix + data.email.messageId,
ACL: 'private',
ContentType: 'text/plain',
StorageClass: 'STANDARD'
}, function(err) {
if (err) {
data.log({level: "error", message: "copyObject() returned error:",
error: err, stack: err.stack});
return data.context.fail("Error: Could not make readable copy of email.");
}
// Load the raw email from S3
data.s3.getObject({
Bucket: data.config.emailBucket,
Key: data.config.emailKeyPrefix + data.email.messageId
}, function(err, result) {
if (err) {
data.log({level: "error", message: "getObject() returned error:",
error: err, stack: err.stack});
return data.context.fail("Error: Failed to load message body from S3.");
}
data.emailData = result.Body.toString();
next(null, data);
});
});
};
/**
* Processes the message data, making updates to recipients and other headers
* before forwarding message.
*
* @param {object} data - Data bundle with context, email, etc.
* @param {function} next - Callback function invoked as (error, data).
*/
exports.processMessage = function(data, next) {
var sp = data.emailData.split(/\r?\n\r?\n/m, 1);
var header = sp[0];
var body = data.emailData.substr(header.length);
var headerItems = parseMailHeader(header);
for (var i in headerItems) {
var name = headerItems[i].name.toLowerCase();
// data.log({level: "info", message: "Process Header ->" + name});
if (name == 'from') {
headerItems[i].lines = [' ' + data.config.fromEmail];
}
else if (name == 'reply-to') {
headerItems[i].lines = [];
}
else if (name == 'return-path') {
headerItems[i].lines = [];
}
else if (name == 'sender') {
headerItems[i].lines = [];
}
else if (name == 'errors-to') {
headerItems[i].lines = [];
}
else if (name == 'dkim-signature') {
headerItems[i].lines = [];
}
}
header = rebuildMailHeader(headerItems);
// data.log({level: "info", message: "Output Header ->" + header});
data.emailData = header + body;
next(null, data);
};
/**
* Send email using the SES sendRawEmail command.
*
* @param {object} data - Data bundle with context, email, etc.
* @param {function} next - Callback function invoked as (error, data).
*/
exports.sendMessage = function(data, next) {
var params = {
Destinations: data.recipients,
Source: data.originalRecipient,
RawMessage: {
Data: data.emailData
}
};
data.log({level: "info", message: "sendMessage: Sending email via SES. " +
"Original recipients: " + data.originalRecipients.join(", ") +
". Transformed recipients: " + data.recipients.join(", ") + "."});
data.ses.sendRawEmail(params, function(err, result) {
if (err) {
data.log({level: "error", message: "sendRawEmail() returned error.",
error: err, stack: err.stack});
data.context.fail('Error: Email sending failed.');
} else {
data.log({level: "info", message: "sendRawEmail() successful.",
result: result});
next(null, data);
}
});
};
/**
* Report success after all steps are complete.
*
* @param {object} data - Data bundle with context.
*/
exports.finish = function(data) {
data.log({level: "info", message: "Process finished successfully."});
data.context.succeed();
};
/**
* Handler function to be invoked by AWS Lambda with an inbound SES email as
* the event.
*
* @param {object} event - Lambda event from inbound email received by AWS SES.
* @param {object} context - Lambda context object.
* @param {object} overrides - Overrides for the default data, including the
* configuration, SES object, and S3 object.
*/
exports.handler = function(event, context, overrides) {
var steps = overrides && overrides.steps ? overrides.steps :
[
exports.parseEvent,
exports.transformRecipients,
exports.fetchMessage,
exports.processMessage,
exports.sendMessage
];
var step;
var currentStep = 0;
var AWS = require('aws-sdk');
var data = {
event: event,
context: context,
config: overrides && overrides.config ? overrides.config : defaultConfig,
log: overrides && overrides.log ? overrides.log : console.log,
ses: overrides && overrides.ses ? overrides.ses : new AWS.SES(),
s3: overrides && overrides.s3 ? overrides.s3 : new AWS.S3()
};
var nextStep = function(err, data) {
if (err) {
data.log({level: "error", message: "Step (index " + (currentStep - 1) +
") returned error:", error: err, stack: err.stack});
context.fail("Error: Step returned error.");
} else if (steps[currentStep]) {
if (typeof steps[currentStep] === "function") {
step = steps[currentStep];
} else {
return context.fail("Error: Invalid step encountered.");
}
currentStep++;
step(data, nextStep);
} else {
// No more steps exist, so invoke the finish function.
exports.finish(data);
}
};
nextStep(null, data);
};
@fstdesk

This comment has been minimized.

Copy link

fstdesk commented May 19, 2017

hi,

i tested on lambda and got that error. thanks.

{
"errorMessage": "Error: Received invalid SES message."
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.