Guest User

Untitled

a guest
Jan 19th, 2019
95
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.13 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 exceptio"))
  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, SOMETHING, newData);
  96. } catch (e) {
  97. try {
  98. notifyBounce(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. const blacklist = email.commonHeaders.to
  162. .concat(email.commonHeaders.from)
  163. .concat(email.commonHeaders.cc)
  164. .concat(email.commonHeaders.bcc)
  165. .map(name => name.toLowerCase());
  166. return newRecipients.filter(function(recipient) {
  167. for (var i = 0; i < blacklist.length; i++) {
  168. if (blacklist[i].indexOf(recipient) !== -1) {
  169. console.log(blacklist[i] + " contains " + recipient + " so it is being removed");
  170. return false;
  171. }
  172. }
  173. return true;
  174. });
  175. }
  176.  
  177. /**
  178. * Fetches the message data from S3.
  179. *
  180. * @param {object} data - Data bundle with context, email, etc.
  181. *
  182. * @return {object} - Promise resolved with data.
  183. */
  184. async function fetchMessage(s3, email) {
  185. console.log({level: "info", message: "Fetching email at s3://" +
  186. data.config.emailBucket + '/' + data.config.emailKeyPrefix +
  187. data.email.messageId});
  188.  
  189. // Copying email object to ensure read permission
  190. const copy = new Promise(function(resolve, reject) {
  191. s3.copyObject({
  192. Bucket: config.emailBucket,
  193. CopySource: config.emailBucket + '/' + config.emailKeyPrefix +
  194. email.messageId,
  195. Key: config.emailKeyPrefix + email.messageId,
  196. ACL: 'private',
  197. ContentType: 'text/plain',
  198. StorageClass: 'STANDARD'
  199. }, function(err) {
  200. if (err) {
  201. console.log({level: "error", message: "copyObject() returned error:"});
  202. reject(err);
  203. } else {
  204. resolve();
  205. }
  206. });
  207. });
  208. await copy;
  209.  
  210. // Load the raw email from S3
  211. const getObject = new Promise(function(resolve, reject) {
  212. s3.getObject({
  213. Bucket: config.emailBucket,
  214. Key: config.emailKeyPrefix + email.messageId
  215. }, function(err, result) {
  216. if (err) {
  217. console.log({level: "error", message: "getObject() returned error:"});
  218. reject(err);
  219. } else {
  220. resolve(result);
  221. }
  222. });
  223. });
  224.  
  225. const result = await(getObject);
  226. return result.Body.toString();
  227. }
  228.  
  229. /**
  230. * Processes the message data, making updates to recipients and other headers
  231. * before forwarding message.
  232. */
  233. async function processMessage(emailData) {
  234. var match = emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
  235. var header = match && match[1] ? match[1] : emailData;
  236. var body = match && match[2] ? match[2] : '';
  237.  
  238. // Add "Reply-To:" with the "From" address if it doesn't already exists
  239. if (!/^Reply-To: /mi.test(header)) {
  240. match = header.match(/^From: (.*(?:\r?\n\s+.*)*\r?\n)/m);
  241. var from = match && match[1] ? match[1] : '';
  242. if (from) {
  243. header = header + 'Reply-To: ' + from;
  244. console.log({level: "info", message: "Added Reply-To address of: " + from});
  245. } else {
  246. console.log({level: "info", message: "Reply-To address not added because " +
  247. "From address was not properly extracted."});
  248. }
  249. }
  250.  
  251. // SES does not allow sending messages from an unverified address,
  252. // so replace the message's "From:" header with the original
  253. // recipient (which is a verified domain)
  254. header = header.replace(
  255. /^From: (.*(?:\r?\n\s+.*)*)/mg,
  256. function(match, from) {
  257. return 'From: ' + from.replace(/<(.*)>/, '').trim() +
  258. ' <' + data.config.fromEmail + '>';
  259. });
  260.  
  261. // // Remove the Return-Path header.
  262. // header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');
  263. //
  264. // // Remove Sender header.
  265. // header = header.replace(/^Sender: (.*)\r?\n/mg, '');
  266.  
  267. // Remove Message-ID header.
  268. header = header.replace(/^Message-ID: (.*)\r?\n/mig, '');
  269.  
  270. // Remove all DKIM-Signature headers to prevent triggering an
  271. // "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
  272. // These signatures will likely be invalid anyways, since the From
  273. // header was modified.
  274. header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');
  275.  
  276. return header + body;
  277. };
  278.  
  279. /**
  280. * Send email using the SES sendRawEmail command.
  281. *
  282. * @param {object} data - Data bundle with context, email, etc.
  283. *
  284. * @return {object} - Promise resolved with data.
  285. */
  286. async function sendMessage(ses, recipients, source, emailData) {
  287. var promises = [];
  288.  
  289. // SES limits 50 receivers per `sendRawEmail`.
  290. for (var i = 0; i < recipients.length; i += 50) {
  291. var params = {
  292. Destinations: recipients.slice(i, i + 50),
  293. Source: source,
  294. RawMessage: {
  295. Data: emailData
  296. }
  297. };
  298. console.log({level: "info", message: "sendMessage: Sending email via SES. " +
  299. "Recipients: " + recipients.join(", ")});
  300. promises.push(new Promise(function(resolve, reject) {
  301. ses.sendRawEmail(params, function(err, result) {
  302. if (err) {
  303. console.log({level: "error", message: "sendRawEmail() returned error."});
  304. reject(err);
  305. } else {
  306. console.log({level: "info", message: "sendRawEmail() successful.",
  307. result: result});
  308. resolve();
  309. }
  310. });
  311. }));
  312. }
  313.  
  314. await Promise.all(promises);
  315. }
  316.  
  317. async function notifyBounce(email) {
  318. const opts = {
  319. BounceSender: config.fromEmail,
  320. BouncedRecipientInfoList: email.from.map(from => {
  321. {
  322. Recipient: from,
  323. BounceType: 'TemporaryFailure',
  324. }
  325. }),
  326. OriginalMessageId: email.commonHeaders.messageId,
  327. };
  328. console.log('bounce opts', opts);
  329. const bounce = new Promise((resolve, reject) => {
  330. ses.sendBounce(opts, function(err, data) {
  331. if (err) {
  332. console.log("failed to send bounce");
  333. reject(err);
  334. } else {
  335. resolve(data);
  336. }
  337. });
  338. });
  339. await bounce;
  340. }
Add Comment
Please, Sign In to add comment