From da285254e0cb56dd3b20492bfc8ab0a6546e585a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20T=2E=20Jarl=C3=B8v?= Date: Sat, 17 Oct 2020 05:45:26 +0200 Subject: [PATCH 1/5] Auto response when no recipient or mail > 10MB --- index.js | 173 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 152 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 5f0f5fe..e77a845 100644 --- a/index.js +++ b/index.js @@ -7,10 +7,49 @@ console.log("AWS Lambda SES Forwarder // @arithmetric // Version 5.0.0"); // Configure the S3 bucket and key prefix for stored raw emails, and the // mapping of email addresses to forward from and to. // +// IAM access: +// { +// "Version": "2020-01-01", +// "Statement": [ +// { +// "Effect": "Allow", +// "Action": [ +// "logs:CreateLogGroup", +// "logs:CreateLogStream", +// "logs:PutLogEvents" +// ], +// "Resource": "arn:aws:logs:*:*:*" +// }, +// { +// "Effect": "Allow", +// "Action": [ +// "ses:SendRawEmail", +// "ses:SendEmail" +// ], +// "Resource": "*" +// }, +// { +// "Effect": "Allow", +// "Action": [ +// "s3:GetObject", +// "s3:PutObject" +// ], +// "Resource": "arn:aws:s3:::s3-bucket-name/emailsPrefix/*" +// } +// ] +// } +// // Expected keys/values: // // - fromEmail: Forwarded emails will come from this verified address // +// - notifyEmail: This will be used to notify the sender, if the mail could not +// be delivered. +// +// - notify550: Enables auto response when email address was not found. +// +// - notify552: Enables auto response if mail is larger than 10 MB. +// // - subjectPrefix: Forwarded emails subject will contain this prefix // // - emailBucket: S3 bucket name where SES stores emails. @@ -37,6 +76,9 @@ console.log("AWS Lambda SES Forwarder // @arithmetric // Version 5.0.0"); // To match all email addresses matching no other mapping, use "@" as a key. var defaultConfig = { fromEmail: "noreply@example.com", + notifyEmail: "MAILER-DAEMON@example.com", + notify550: true, + notify552: true, subjectPrefix: "", emailBucket: "s3-bucket-name", emailKeyPrefix: "emailsPrefix/", @@ -138,7 +180,11 @@ exports.transformRecipients = function(data) { "original destinations: " + data.originalRecipients.join(", "), level: "info" }); - return data.callback(); + if (data.config.notify550) { + data.smtpErr = "550"; + } else { + return data.callback(); + } } data.recipients = newRecipients; @@ -195,6 +241,23 @@ exports.fetchMessage = function(data) { return reject( new Error("Error: Failed to load message body from S3.")); } + // Check content lenght (SES hardcoded 10 MB - 10_000_000 bytes) + if (result.ContentLength >= 10000000) { + if (data.config.notify552) { + data.smtpErr = "552", + data.log({ + level: "info", + message: "ContentLength > 10 MB, size = " + result.ContentLength + " bytes (SMTP 552)" + }); + } else { + data.log({ + level: "error", + message: "ContentLength > 10 MB, size = " + result.ContentLength + " bytes (SMTP 552)" + }); + return reject( + new Error("Error: Mail size exceeds 10 MB.")); + } + } data.emailData = result.Body.toString(); return resolve(data); }); @@ -221,6 +284,7 @@ exports.processMessage = function(data) { var from = match && match[1] ? match[1] : ''; if (from) { header = header + 'Reply-To: ' + from; + data.from = from; data.log({ level: "info", message: "Added Reply-To address of: " + from @@ -256,6 +320,7 @@ exports.processMessage = function(data) { header = header.replace( /^subject:[\t ]?(.*)/mgi, function(match, subject) { + data.subject = subject; return 'Subject: ' + data.config.subjectPrefix + subject; }); } @@ -299,30 +364,93 @@ exports.sendMessage = function(data) { Data: data.emailData } }; - data.log({ - level: "info", - message: "sendMessage: Sending email via SES. Original recipients: " + - data.originalRecipients.join(", ") + ". Transformed recipients: " + - data.recipients.join(", ") + "." - }); + + // Format params if we are bouncing + if (data.smtpErr != "" && data.config.notifyEmail != "") { + var bounceData = ""; + switch (data.smtpErr) { + case '552': + bounceData = "Your email was rejected. Please ensure that the size of your mail is less than 10 MB.\n\n" + "SMTP Reply Code = 552, SMTP Status Code = 5.3.4"; + break; + case '550': + bounceData = "Your email was rejected. The email address was not found. Please check the receiving email address.\n\n" + "SMTP Reply Code = 550, SMTP Status Code = 5.1.1"; + break; + default: + bounceData = "Unknown error" + } + + params = { + Destination: { + ToAddresses: data.from.split() + }, + Source: data.config.notifyEmail, + Message: { + Subject: { + Data: "Delivery Status Notification (Failure)" + }, + Body: { + Text: { + Data: "An error occurred while trying to deliver the mail to the following recipients: " + data.originalRecipients + "\n\n" + bounceData + } + } + } + }; + + data.log({ + level: "info", + message: "sendMessage: Sending bounce email via SES. Recipients: " + + data.originalRecipients + ". Bounce code: " + data.smtpErr + }); + } else { + data.log({ + level: "info", + message: "sendMessage: Sending email via SES. Original recipients: " + + data.originalRecipients.join(", ") + ". Transformed recipients: " + + data.recipients.join(", ") + "." + }); + } + return new Promise(function(resolve, reject) { - data.ses.sendRawEmail(params, function(err, result) { - if (err) { + if (data.smtpErr != "" && data.config.notifyEmail != "") { + // If bounce + data.ses.sendEmail(params, function(err, result) { + if (err) { + data.log({ + level: "error", + message: "sendRawEmail() data.smtpErr returned error.", + error: err, + stack: err.stack + }); + return reject(new Error('Error: Email sending failed.')); + } data.log({ - level: "error", - message: "sendRawEmail() returned error.", - error: err, - stack: err.stack + level: "info", + message: "sendRawEmail() data.smtpErr successful.", + result: result }); - return reject(new Error('Error: Email sending failed.')); - } - data.log({ - level: "info", - message: "sendRawEmail() successful.", - result: result + resolve(data); }); - resolve(data); - }); + + } else { + // If OK + data.ses.sendRawEmail(params, function(err, result) { + if (err) { + data.log({ + level: "error", + message: "sendRawEmail() returned error.", + error: err, + stack: err.stack + }); + return reject(new Error('Error: Email sending failed.')); + } + data.log({ + level: "info", + message: "sendRawEmail() successful.", + result: result + }); + resolve(data); + }); + } }); }; @@ -349,6 +477,9 @@ exports.handler = function(event, context, callback, overrides) { event: event, callback: callback, context: context, + from: "", + subject: "", + smtpErr: "", config: overrides && overrides.config ? overrides.config : defaultConfig, log: overrides && overrides.log ? overrides.log : console.log, ses: overrides && overrides.ses ? overrides.ses : new AWS.SES(), From a5ce2dd91d03215e1235bf5a9dc51737aae7d094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20T=2E=20Jarl=C3=B8v?= Date: Sat, 17 Oct 2020 06:00:26 +0200 Subject: [PATCH 2/5] Update transformRecipients.js --- test/transformRecipients.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/transformRecipients.js b/test/transformRecipients.js index d21ebcd..b791776 100644 --- a/test/transformRecipients.js +++ b/test/transformRecipients.js @@ -218,5 +218,29 @@ describe('index.js', function() { done(); }); }); + + it('should give error code 550', + function(done) { + var data = { + recipients: ["norecipient@foo.com"], + config: { + forwardMapping: { + "info@foo.com": [ + "jim@example.com" + ] + }, + notifyEmail: "MAILER-DAEMON@example.com", + notify550: true + }, + log: console.log + }; + index.transformRecipients(data) + .then(function(data) { + assert.equal(data.config.notify550, + "550", + "smpt error code enabled"); + done(); + }); + }); }); }); From f87bfff66888e9a306e94fb9624894e867109d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20T=2E=20Jarl=C3=B8v?= Date: Sat, 17 Oct 2020 06:10:03 +0200 Subject: [PATCH 3/5] Update transformRecipients.js --- test/transformRecipients.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/transformRecipients.js b/test/transformRecipients.js index b791776..3f610a7 100644 --- a/test/transformRecipients.js +++ b/test/transformRecipients.js @@ -236,9 +236,9 @@ describe('index.js', function() { }; index.transformRecipients(data) .then(function(data) { - assert.equal(data.config.notify550, + assert.equal(data.smtpErr, "550", - "smpt error code enabled"); + "smpt error code 550 enabled"); done(); }); }); From 96204004726a431bbaad384bb3d8aac31d739823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20T=2E=20Jarl=C3=B8v?= Date: Sat, 17 Oct 2020 06:11:49 +0200 Subject: [PATCH 4/5] Update fetchMessage.js --- test/fetchMessage.js | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/test/fetchMessage.js b/test/fetchMessage.js index 9854f0f..6134c6f 100644 --- a/test/fetchMessage.js +++ b/test/fetchMessage.js @@ -97,5 +97,77 @@ describe('index.js', function() { done(); }); }); + + it('should enable smtp error 552', + function(done) { + var data = { + config: { + notifyEmail: "MAILER-DAEMON@example.com", + notify552: true, + emailBucket: "bucket", + emailKeyPrefix: "prefix/" + }, + context: { + fail: function() { + assert.ok(false, 'context.fail() was called'); + done(); + } + }, + email: { + messageId: "abc" + }, + log: console.log, + s3: { + copyObject: function(options, callback) { + callback(null); + }, + getObject: function(options, callback) { + callback(null, {Body: "email data", ContentLength: 20000000}); + } + } + }; + index.fetchMessage(data) + .then(function(data) { + assert.equal(data.smtpErr, + "552", + "fetchMessage returned email data"); + done(); + }); + }); + + it('should fail due to mail size exceeds 10MB', + function(done) { + var data = { + config: { + notifyEmail: "MAILER-DAEMON@example.com", + notify552: false, + emailBucket: "bucket", + emailKeyPrefix: "prefix/" + }, + context: { + fail: function() { + assert.ok(false, 'context.fail() was called'); + done(); + } + }, + email: { + messageId: "abc" + }, + log: console.log, + s3: { + copyObject: function(options, callback) { + callback(null); + }, + getObject: function(options, callback) { + callback(null, {Body: "email data", ContentLength: 20000000}); + } + } + }; + index.fetchMessage(data) + .catch(function(err) { + assert.ok(err, "fetchMessage aborted operation"); + done(); + }); + }); }); }); From e027a3093939881082746e533bc26fbb2fb00905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20T=2E=20Jarl=C3=B8v?= Date: Sat, 17 Oct 2020 06:25:48 +0200 Subject: [PATCH 5/5] Update sendMessage.js --- test/sendMessage.js | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/sendMessage.js b/test/sendMessage.js index d487fff..3318c98 100644 --- a/test/sendMessage.js +++ b/test/sendMessage.js @@ -56,5 +56,67 @@ describe('index.js', function() { done(); }); }); + + it('should invoke the AWS SES SDK to send the bounce message', + function(done) { + var data = { + config: { + notifyEmail: "MAILER-DAEMON@example.com", + notify550: true, + notify552: true, + }, + smtpErr: "552", + recipients: [ + "jim@example.com" + ], + originalRecipients: [ + "info@example.com" + ], + emailData: "message data", + context: {}, + log: console.log, + ses: { + sendEmail: function(options, callback) { + callback(null, {status: "ok"}); + } + } + }; + index.sendMessage(data) + .then(function() { + assert.ok(true, "sendMessage returned successfully"); + done(); + }); + }); + + it('should result in failure if the AWS SES SDK cannot send the bounce message', + function(done) { + var data = { + config: { + notifyEmail: "MAILER-DAEMON@example.com", + notify550: true, + notify552: true, + }, + smtpErr: "550", + recipients: [ + "jim@example.com" + ], + originalRecipients: [ + "info@example.com" + ], + emailData: "message data", + context: {}, + log: console.log, + ses: { + sendEmail: function(options, callback) { + callback(true); + } + } + }; + index.sendMessage(data) + .catch(function(err) { + assert.ok(err, "sendMessage bounce msg aborted operation"); + done(); + }); + }); }); });