Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- "use strict";
- var AWS = require('aws-sdk');
- console.log("AWS Lambda SES Forwarder // @arithmetric // Version 4.2.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
- //
- // - subjectPrefix: Forwarded emails subject will contain this prefix
- //
- // - emailBucket: S3 bucket name where SES stores emails.
- //
- // - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
- // trailing slash.
- //
- // - 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.
- //
- // 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`).
- //
- // 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`).
- const config = {
- fromEmail: "noreply@rust-lang.com",
- emailBucket: "rustlang-emails",
- emailKeyPrefix: "incoming/",
- forwardMapping: {
- "admin@rust-lang.com": [
- "alex@alexcrichton.com",
- ],
- "webmaster@rust-lang.com": [
- "admin@rust-lang.com",
- ],
- "infra@rust-lang.com": [
- "pietro@pietroalbini.org",
- "alex@alexcrichton.com",
- ],
- }
- };
- /**
- * 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} callback - Lambda callback object.
- * @param {object} overrides - Overrides for the default data, including the
- * configuration, SES object, and S3 object.
- */
- exports.handler = function(event, context, callback, overrides) {
- handle(event, context)
- .then(() => {
- console.log({level: "invo", message: "process finished successfully"});
- callback();
- })
- .catch(err => {
- console.log({
- level: "error",
- message: "exception throwin while processing",
- error: err,
- stack: err.stack,
- });
- callback(new Error("error: step threw an exceptio"))
- })
- };
- async function handle(event, context) {
- const ses = new AWS.SES();
- const s3 = new AWS.S3({signatureVersion: 'v4'});
- const { email, recipients } = parseEvent(event);
- try {
- const newRecipients = transformRecipients(recipients, email);
- if (!newRecipients.length) {
- console.log({message: "Finishing process. No new recipients found for " +
- "original destinations: " + recipients.join(", "),
- level: "info"});
- return
- }
- const emailData = await fetchMessage(s3, email);
- if (emailData.indexOf("lolwut") !== -1) {
- throw new Error('lolwut');
- }
- const newData = processMessage(emailData);
- sendMessage(ses, newRecipients, SOMETHING, newData);
- } catch (e) {
- try {
- notifyBounce(email);
- } catch (bounce_err) {
- console.log({
- message: "failed to bounce",
- err: bounce_err,
- stack: bounce_err.stack,
- level: "error"
- });
- }
- throw e;
- }
- }
- /**
- * Parses the SES event record provided for the `email` and `receipients` data.
- */
- function parseEvent(event) {
- // Validate characteristics of a SES event record.
- if (!event ||
- !event.hasOwnProperty('Records') ||
- event.Records.length !== 1 ||
- !event.Records[0].hasOwnProperty('eventSource') ||
- event.Records[0].eventSource !== 'aws:ses' ||
- event.Records[0].eventVersion !== '1.0')
- {
- console.log({message: "parseEvent() received invalid SES message:",
- level: "error", event: JSON.stringify(event)});
- throw new Error('Error: Received invalid SES message.');
- }
- const ret = {};
- ret.email = event.Records[0].ses.mail;
- ret.recipients = event.Records[0].ses.receipt.recipients;
- return ret;
- };
- /**
- * Transforms the original recipients to the desired forwarded destinations.
- */
- function transformRecipients(recipients, email) {
- var newRecipients = [];
- var changed = true;
- while (changed) {
- newRecipients = [];
- changed = false;
- recipients.forEach(function(origEmail) {
- var origEmailKey = origEmail.toLowerCase();
- if (config.forwardMapping.hasOwnProperty(origEmailKey)) {
- newRecipients = newRecipients.concat(
- config.forwardMapping[origEmailKey]);
- //data.originalRecipient = origEmail;
- changed = true;
- } else {
- newRecipients.push(origEmail);
- }
- });
- recipients = newRecipients;
- }
- // Don't send this email back to whomever it was sent from. Also don't send
- // the email again to folks already receiving it.
- console.log(email.commonHeaders);
- console.log(email);
- const blacklist = email.commonHeaders.to
- .concat(email.commonHeaders.from)
- .concat(email.commonHeaders.cc)
- .concat(email.commonHeaders.bcc)
- .map(name => name.toLowerCase());
- return newRecipients.filter(function(recipient) {
- for (var i = 0; i < blacklist.length; i++) {
- if (blacklist[i].indexOf(recipient) !== -1) {
- console.log(blacklist[i] + " contains " + recipient + " so it is being removed");
- return false;
- }
- }
- return true;
- });
- }
- /**
- * Fetches the message data from S3.
- *
- * @param {object} data - Data bundle with context, email, etc.
- *
- * @return {object} - Promise resolved with data.
- */
- async function fetchMessage(s3, email) {
- console.log({level: "info", message: "Fetching email at s3://" +
- data.config.emailBucket + '/' + data.config.emailKeyPrefix +
- data.email.messageId});
- // Copying email object to ensure read permission
- const copy = new Promise(function(resolve, reject) {
- s3.copyObject({
- Bucket: config.emailBucket,
- CopySource: config.emailBucket + '/' + config.emailKeyPrefix +
- email.messageId,
- Key: config.emailKeyPrefix + email.messageId,
- ACL: 'private',
- ContentType: 'text/plain',
- StorageClass: 'STANDARD'
- }, function(err) {
- if (err) {
- console.log({level: "error", message: "copyObject() returned error:"});
- reject(err);
- } else {
- resolve();
- }
- });
- });
- await copy;
- // Load the raw email from S3
- const getObject = new Promise(function(resolve, reject) {
- s3.getObject({
- Bucket: config.emailBucket,
- Key: config.emailKeyPrefix + email.messageId
- }, function(err, result) {
- if (err) {
- console.log({level: "error", message: "getObject() returned error:"});
- reject(err);
- } else {
- resolve(result);
- }
- });
- });
- const result = await(getObject);
- return result.Body.toString();
- }
- /**
- * Processes the message data, making updates to recipients and other headers
- * before forwarding message.
- */
- async function processMessage(emailData) {
- var match = emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
- var header = match && match[1] ? match[1] : emailData;
- var body = match && match[2] ? match[2] : '';
- // Add "Reply-To:" with the "From" address if it doesn't already exists
- if (!/^Reply-To: /mi.test(header)) {
- match = header.match(/^From: (.*(?:\r?\n\s+.*)*\r?\n)/m);
- var from = match && match[1] ? match[1] : '';
- if (from) {
- header = header + 'Reply-To: ' + from;
- console.log({level: "info", message: "Added Reply-To address of: " + from});
- } else {
- console.log({level: "info", message: "Reply-To address not added because " +
- "From address was not properly extracted."});
- }
- }
- // SES does not allow sending messages from an unverified address,
- // so replace the message's "From:" header with the original
- // recipient (which is a verified domain)
- header = header.replace(
- /^From: (.*(?:\r?\n\s+.*)*)/mg,
- function(match, from) {
- return 'From: ' + from.replace(/<(.*)>/, '').trim() +
- ' <' + data.config.fromEmail + '>';
- });
- // // Remove the Return-Path header.
- // header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');
- //
- // // Remove Sender header.
- // header = header.replace(/^Sender: (.*)\r?\n/mg, '');
- // Remove Message-ID header.
- header = header.replace(/^Message-ID: (.*)\r?\n/mig, '');
- // Remove all DKIM-Signature headers to prevent triggering an
- // "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
- // These signatures will likely be invalid anyways, since the From
- // header was modified.
- header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');
- return header + body;
- };
- /**
- * Send email using the SES sendRawEmail command.
- *
- * @param {object} data - Data bundle with context, email, etc.
- *
- * @return {object} - Promise resolved with data.
- */
- async function sendMessage(ses, recipients, source, emailData) {
- var promises = [];
- // SES limits 50 receivers per `sendRawEmail`.
- for (var i = 0; i < recipients.length; i += 50) {
- var params = {
- Destinations: recipients.slice(i, i + 50),
- Source: source,
- RawMessage: {
- Data: emailData
- }
- };
- console.log({level: "info", message: "sendMessage: Sending email via SES. " +
- "Recipients: " + recipients.join(", ")});
- promises.push(new Promise(function(resolve, reject) {
- ses.sendRawEmail(params, function(err, result) {
- if (err) {
- console.log({level: "error", message: "sendRawEmail() returned error."});
- reject(err);
- } else {
- console.log({level: "info", message: "sendRawEmail() successful.",
- result: result});
- resolve();
- }
- });
- }));
- }
- await Promise.all(promises);
- }
- async function notifyBounce(email) {
- const opts = {
- BounceSender: config.fromEmail,
- BouncedRecipientInfoList: email.from.map(from => {
- {
- Recipient: from,
- BounceType: 'TemporaryFailure',
- }
- }),
- OriginalMessageId: email.commonHeaders.messageId,
- };
- console.log('bounce opts', opts);
- const bounce = new Promise((resolve, reject) => {
- ses.sendBounce(opts, function(err, data) {
- if (err) {
- console.log("failed to send bounce");
- reject(err);
- } else {
- resolve(data);
- }
- });
- });
- await bounce;
- }
Add Comment
Please, Sign In to add comment