Guest User

Untitled

a guest
Jan 19th, 2019
91
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.35 KB | None | 0 0
  1. "use strict";
  2.  
  3. var AWS = require('aws-sdk');
  4.  
  5. console.log("AWS Lambda SES Forwarder // @arithmetric // Version 4.2.0");
  6.  
  7. // Configure the S3 bucket and key prefix for stored raw emails, and the
  8. // mapping of email addresses to forward from and to.
  9. //
  10. // Expected keys/values:
  11. //
  12. // - fromEmail: Forwarded emails will come from this verified address
  13. //
  14. // - subjectPrefix: Forwarded emails subject will contain this prefix
  15. //
  16. // - emailBucket: S3 bucket name where SES stores emails.
  17. //
  18. // - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
  19. // trailing slash.
  20. //
  21. // - forwardMapping: Object where the key is the lowercase email address from
  22. // which to forward and the value is an array of email addresses to which to
  23. // send the message.
  24. //
  25. // To match all email addresses on a domain, use a key without the name part
  26. // of an email address before the "at" symbol (i.e. `@example.com`).
  27. //
  28. // To match a mailbox name on all domains, use a key without the "at" symbol
  29. // and domain part of an email address (i.e. `info`).
  30. const config = {
  31. fromEmail: "noreply@rust-lang.com",
  32. emailBucket: "rustlang-emails",
  33. emailKeyPrefix: "incoming/",
  34. forwardMapping: {
  35. "admin@rust-lang.com": [
  36. "alex@alexcrichton.com",
  37. ],
  38. "webmaster@rust-lang.com": [
  39. "admin@rust-lang.com",
  40. ],
  41. "infra@rust-lang.com": [
  42. "pietro@pietroalbini.org",
  43. "alex@alexcrichton.com",
  44. ],
  45. }
  46. };
  47.  
  48.  
  49. /**
  50. * Handler function to be invoked by AWS Lambda with an inbound SES email as
  51. * the event.
  52. *
  53. * @param {object} event - Lambda event from inbound email received by AWS SES.
  54. * @param {object} context - Lambda context object.
  55. * @param {object} callback - Lambda callback object.
  56. * @param {object} overrides - Overrides for the default data, including the
  57. * configuration, SES object, and S3 object.
  58. */
  59. exports.handler = function(event, context, callback, overrides) {
  60. handle(event, context)
  61. .then(() => {
  62. console.log({level: "invo", message: "process finished successfully"});
  63. callback();
  64. })
  65. .catch(err => {
  66. console.log({
  67. level: "error",
  68. message: "exception throwin while processing",
  69. error: err,
  70. stack: err.stack,
  71. });
  72. callback(new Error("error: step threw an exception"));
  73. });
  74. };
  75.  
  76. async function handle(event, context) {
  77. const ses = new AWS.SES();
  78. const s3 = new AWS.S3({signatureVersion: 'v4'});
  79. const { email, recipients } = parseEvent(event);
  80. try {
  81. const newRecipients = transformRecipients(recipients, email);
  82. if (!newRecipients.length) {
  83. console.log({message: "Finishing process. No new recipients found for " +
  84. "original destinations: " + recipients.join(", "),
  85. level: "info"});
  86. return;
  87. }
  88.  
  89. const emailData = await fetchMessage(s3, email);
  90. if (emailData.indexOf("lolwut") !== -1) {
  91. throw new Error('lolwut');
  92. }
  93.  
  94. const newData = processMessage(emailData);
  95. sendMessage(ses, newRecipients, email.commonHeaders.from[0], newData);
  96. } catch (e) {
  97. try {
  98. notifyBounce(ses, email);
  99. } catch (bounce_err) {
  100. console.log({
  101. message: "failed to bounce",
  102. err: bounce_err,
  103. stack: bounce_err.stack,
  104. level: "error"
  105. });
  106. }
  107. throw e;
  108. }
  109. }
  110.  
  111. /**
  112. * Parses the SES event record provided for the `email` and `receipients` data.
  113. */
  114. function parseEvent(event) {
  115. // Validate characteristics of a SES event record.
  116. if (!event ||
  117. !event.hasOwnProperty('Records') ||
  118. event.Records.length !== 1 ||
  119. !event.Records[0].hasOwnProperty('eventSource') ||
  120. event.Records[0].eventSource !== 'aws:ses' ||
  121. event.Records[0].eventVersion !== '1.0')
  122. {
  123. console.log({message: "parseEvent() received invalid SES message:",
  124. level: "error", event: JSON.stringify(event)});
  125. throw new Error('Error: Received invalid SES message.');
  126. }
  127.  
  128. const ret = {};
  129. ret.email = event.Records[0].ses.mail;
  130. ret.recipients = event.Records[0].ses.receipt.recipients;
  131. return ret;
  132. }
  133.  
  134. /**
  135. * Transforms the original recipients to the desired forwarded destinations.
  136. */
  137. function transformRecipients(recipients, email) {
  138. var newRecipients = [];
  139. var changed = true;
  140. while (changed) {
  141. newRecipients = [];
  142. changed = false;
  143. recipients.forEach(function(origEmail) {
  144. var origEmailKey = origEmail.toLowerCase();
  145. if (config.forwardMapping.hasOwnProperty(origEmailKey)) {
  146. newRecipients = newRecipients.concat(
  147. config.forwardMapping[origEmailKey]);
  148. //data.originalRecipient = origEmail;
  149. changed = true;
  150. } else {
  151. newRecipients.push(origEmail);
  152. }
  153. });
  154. recipients = newRecipients;
  155. }
  156.  
  157. // Don't send this email back to whomever it was sent from. Also don't send
  158. // the email again to folks already receiving it.
  159. console.log(email.commonHeaders);
  160. console.log(email);
  161. let blacklist = [];
  162. if (email.commonHeaders.to)
  163. blacklist = blacklist.concat(email.commonHeaders.to);
  164. if (email.commonHeaders.from)
  165. blacklist = blacklist.concat(email.commonHeaders.from);
  166. if (email.commonHeaders.cc)
  167. blacklist = blacklist.concat(email.commonHeaders.cc);
  168. if (email.commonHeaders.bcc)
  169. blacklist = blacklist.concat(email.commonHeaders.bcc);
  170. return newRecipients.filter(function(recipient) {
  171. for (var i = 0; i < blacklist.length; i++) {
  172. if (blacklist[i].indexOf(recipient) !== -1) {
  173. console.log(blacklist[i] + " contains " + recipient + " so it is being removed");
  174. return false;
  175. }
  176. }
  177. return true;
  178. });
  179. }
  180.  
  181. /**
  182. * Fetches the message data from S3.
  183. *
  184. * @param {object} data - Data bundle with context, email, etc.
  185. *
  186. * @return {object} - Promise resolved with data.
  187. */
  188. async function fetchMessage(s3, email) {
  189. console.log({level: "info", message: "Fetching email at s3://" +
  190. config.emailBucket + '/' + config.emailKeyPrefix +
  191. email.messageId});
  192.  
  193. // Copying email object to ensure read permission
  194. const copy = new Promise(function(resolve, reject) {
  195. s3.copyObject({
  196. Bucket: config.emailBucket,
  197. CopySource: config.emailBucket + '/' + config.emailKeyPrefix +
  198. email.messageId,
  199. Key: config.emailKeyPrefix + email.messageId,
  200. ACL: 'private',
  201. ContentType: 'text/plain',
  202. StorageClass: 'STANDARD'
  203. }, function(err) {
  204. if (err) {
  205. console.log({level: "error", message: "copyObject() returned error:"});
  206. reject(err);
  207. } else {
  208. resolve();
  209. }
  210. });
  211. });
  212. await copy;
  213.  
  214. // Load the raw email from S3
  215. const getObject = new Promise(function(resolve, reject) {
  216. s3.getObject({
  217. Bucket: config.emailBucket,
  218. Key: config.emailKeyPrefix + email.messageId
  219. }, function(err, result) {
  220. if (err) {
  221. console.log({level: "error", message: "getObject() returned error:"});
  222. reject(err);
  223. } else {
  224. resolve(result);
  225. }
  226. });
  227. });
  228.  
  229. const result = await(getObject);
  230. return result.Body.toString();
  231. }
  232.  
  233. /**
  234. * Processes the message data, making updates to recipients and other headers
  235. * before forwarding message.
  236. */
  237. async function processMessage(emailData) {
  238. var match = emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
  239. var header = match && match[1] ? match[1] : emailData;
  240. var body = match && match[2] ? match[2] : '';
  241.  
  242. // Add "Reply-To:" with the "From" address if it doesn't already exists
  243. if (!/^Reply-To: /mi.test(header)) {
  244. match = header.match(/^From: (.*(?:\r?\n\s+.*)*\r?\n)/m);
  245. var from = match && match[1] ? match[1] : '';
  246. if (from) {
  247. header = header + 'Reply-To: ' + from;
  248. console.log({level: "info", message: "Added Reply-To address of: " + from});
  249. } else {
  250. console.log({level: "info", message: "Reply-To address not added because " +
  251. "From address was not properly extracted."});
  252. }
  253. }
  254.  
  255. // SES does not allow sending messages from an unverified address,
  256. // so replace the message's "From:" header with the original
  257. // recipient (which is a verified domain)
  258. header = header.replace(
  259. /^From: (.*(?:\r?\n\s+.*)*)/mg,
  260. function(match, from) {
  261. return 'From: ' + from.replace(/<(.*)>/, '').trim() +
  262. ' <' + config.fromEmail + '>';
  263. });
  264.  
  265. // // Remove the Return-Path header.
  266. // header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');
  267. //
  268. // // Remove Sender header.
  269. // header = header.replace(/^Sender: (.*)\r?\n/mg, '');
  270.  
  271. // Remove Message-ID header.
  272. header = header.replace(/^Message-ID: (.*)\r?\n/mig, '');
  273.  
  274. // Remove all DKIM-Signature headers to prevent triggering an
  275. // "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
  276. // These signatures will likely be invalid anyways, since the From
  277. // header was modified.
  278. header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');
  279.  
  280. return header + body;
  281. }
  282.  
  283. /**
  284. * Send email using the SES sendRawEmail command.
  285. *
  286. * @param {object} data - Data bundle with context, email, etc.
  287. *
  288. * @return {object} - Promise resolved with data.
  289. */
  290. async function sendMessage(ses, recipients, source, emailData) {
  291. var promises = [];
  292.  
  293. // SES limits 50 receivers per `sendRawEmail`.
  294. for (var i = 0; i < recipients.length; i += 50) {
  295. var params = {
  296. Destinations: recipients.slice(i, i + 50),
  297. Source: source,
  298. RawMessage: {
  299. Data: emailData
  300. }
  301. };
  302. console.log({level: "info", message: "sendMessage: Sending email via SES. " +
  303. "Recipients: " + recipients.join(", ")});
  304. promises.push(new Promise(function(resolve, reject) {
  305. ses.sendRawEmail(params, function(err, result) {
  306. if (err) {
  307. console.log({level: "error", message: "sendRawEmail() returned error."});
  308. reject(err);
  309. } else {
  310. console.log({level: "info", message: "sendRawEmail() successful.",
  311. result: result});
  312. resolve();
  313. }
  314. });
  315. }));
  316. }
  317.  
  318. await Promise.all(promises);
  319. }
  320.  
  321. async function notifyBounce(ses, email) {
  322. const opts = {
  323. BounceSender: config.fromEmail,
  324. BouncedRecipientInfoList: email.commonHeaders.from.map(from => {
  325. return {
  326. Recipient: from,
  327. BounceType: 'TemporaryFailure',
  328. };
  329. }),
  330. OriginalMessageId: email.commonHeaders.messageId,
  331. };
  332. console.log('bounce opts', opts);
  333. const bounce = new Promise((resolve, reject) => {
  334. ses.sendBounce(opts, function(err, data) {
  335. if (err) {
  336. console.log("failed to send bounce");
  337. reject(err);
  338. } else {
  339. resolve(data);
  340. }
  341. });
  342. });
  343. await bounce;
  344. }
Add Comment
Please, Sign In to add comment