Guest User

Untitled

a guest
Jan 3rd, 2019
131
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.26 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. var defaultConfig = {
  31. fromEmail: "noreply@example.com",
  32. subjectPrefix: "",
  33. emailBucket: "s3-bucket-name",
  34. emailKeyPrefix: "emailsPrefix/",
  35. forwardMapping: {
  36. "info@example.com": [
  37. "example.john@example.com",
  38. "example.jen@example.com"
  39. ],
  40. "abuse@example.com": [
  41. "example.jim@example.com"
  42. ],
  43. "@example.com": [
  44. "example.john@example.com"
  45. ],
  46. "info": [
  47. "info@example.com"
  48. ]
  49. }
  50. };
  51.  
  52. /**
  53. * Parses the SES event record provided for the `mail` and `receipients` data.
  54. *
  55. * @param {object} data - Data bundle with context, email, etc.
  56. *
  57. * @return {object} - Promise resolved with data.
  58. */
  59. exports.parseEvent = function(data) {
  60. // Validate characteristics of a SES event record.
  61. if (!data.event ||
  62. !data.event.hasOwnProperty('Records') ||
  63. data.event.Records.length !== 1 ||
  64. !data.event.Records[0].hasOwnProperty('eventSource') ||
  65. data.event.Records[0].eventSource !== 'aws:ses' ||
  66. data.event.Records[0].eventVersion !== '1.0') {
  67. data.log({message: "parseEvent() received invalid SES message:",
  68. level: "error", event: JSON.stringify(data.event)});
  69. return Promise.reject(new Error('Error: Received invalid SES message.'));
  70. }
  71.  
  72. data.email = data.event.Records[0].ses.mail;
  73. data.recipients = data.event.Records[0].ses.receipt.recipients;
  74. return Promise.resolve(data);
  75. };
  76.  
  77. /**
  78. * Transforms the original recipients to the desired forwarded destinations.
  79. *
  80. * @param {object} data - Data bundle with context, email, etc.
  81. *
  82. * @return {object} - Promise resolved with data.
  83. */
  84. exports.transformRecipients = function(data) {
  85. var newRecipients = [];
  86. data.originalRecipients = data.recipients;
  87. data.recipients.forEach(function(origEmail) {
  88. var origEmailKey = origEmail.toLowerCase();
  89. if (data.config.forwardMapping.hasOwnProperty(origEmailKey)) {
  90. newRecipients = newRecipients.concat(
  91. data.config.forwardMapping[origEmailKey]);
  92. data.originalRecipient = origEmail;
  93. } else {
  94. var origEmailDomain;
  95. var origEmailUser;
  96. var pos = origEmailKey.lastIndexOf("@");
  97. if (pos === -1) {
  98. origEmailUser = origEmailKey;
  99. } else {
  100. origEmailDomain = origEmailKey.slice(pos);
  101. origEmailUser = origEmailKey.slice(0, pos);
  102. }
  103. if (origEmailDomain &&
  104. data.config.forwardMapping.hasOwnProperty(origEmailDomain)) {
  105. newRecipients = newRecipients.concat(
  106. data.config.forwardMapping[origEmailDomain]);
  107. data.originalRecipient = origEmail;
  108. } else if (origEmailUser &&
  109. data.config.forwardMapping.hasOwnProperty(origEmailUser)) {
  110. newRecipients = newRecipients.concat(
  111. data.config.forwardMapping[origEmailUser]);
  112. data.originalRecipient = origEmail;
  113. }
  114. }
  115. });
  116.  
  117. var from = data.email.commonHeaders.from.toLowerCase();
  118. newRecipients = newRecipients.filter(function(recipient) {
  119. return !from.contains(recipient);
  120. });
  121.  
  122. if (!newRecipients.length) {
  123. data.log({message: "Finishing process. No new recipients found for " +
  124. "original destinations: " + data.originalRecipients.join(", "),
  125. level: "info"});
  126. return data.callback();
  127. }
  128.  
  129. data.recipients = newRecipients;
  130. return Promise.resolve(data);
  131. };
  132.  
  133. /**
  134. * Fetches the message data from S3.
  135. *
  136. * @param {object} data - Data bundle with context, email, etc.
  137. *
  138. * @return {object} - Promise resolved with data.
  139. */
  140. exports.fetchMessage = function(data) {
  141. // Copying email object to ensure read permission
  142. data.log({level: "info", message: "Fetching email at s3://" +
  143. data.config.emailBucket + '/' + data.config.emailKeyPrefix +
  144. data.email.messageId});
  145. return new Promise(function(resolve, reject) {
  146. data.s3.copyObject({
  147. Bucket: data.config.emailBucket,
  148. CopySource: data.config.emailBucket + '/' + data.config.emailKeyPrefix +
  149. data.email.messageId,
  150. Key: data.config.emailKeyPrefix + data.email.messageId,
  151. ACL: 'private',
  152. ContentType: 'text/plain',
  153. StorageClass: 'STANDARD'
  154. }, function(err) {
  155. if (err) {
  156. data.log({level: "error", message: "copyObject() returned error:",
  157. error: err, stack: err.stack});
  158. return reject(
  159. new Error("Error: Could not make readable copy of email."));
  160. }
  161.  
  162. // Load the raw email from S3
  163. data.s3.getObject({
  164. Bucket: data.config.emailBucket,
  165. Key: data.config.emailKeyPrefix + data.email.messageId
  166. }, function(err, result) {
  167. if (err) {
  168. data.log({level: "error", message: "getObject() returned error:",
  169. error: err, stack: err.stack});
  170. return reject(
  171. new Error("Error: Failed to load message body from S3."));
  172. }
  173. data.emailData = result.Body.toString();
  174. return resolve(data);
  175. });
  176. });
  177. });
  178. };
  179.  
  180. /**
  181. * Processes the message data, making updates to recipients and other headers
  182. * before forwarding message.
  183. *
  184. * @param {object} data - Data bundle with context, email, etc.
  185. *
  186. * @return {object} - Promise resolved with data.
  187. */
  188. exports.processMessage = function(data) {
  189. var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
  190. var header = match && match[1] ? match[1] : data.emailData;
  191. var body = match && match[2] ? match[2] : '';
  192.  
  193. // Add "Reply-To:" with the "From" address if it doesn't already exists
  194. if (!/^Reply-To: /mi.test(header)) {
  195. match = header.match(/^From: (.*(?:\r?\n\s+.*)*\r?\n)/m);
  196. var from = match && match[1] ? match[1] : '';
  197. if (from) {
  198. header = header + 'Reply-To: ' + from;
  199. data.log({level: "info", message: "Added Reply-To address of: " + from});
  200. } else {
  201. data.log({level: "info", message: "Reply-To address not added because " +
  202. "From address was not properly extracted."});
  203. }
  204. }
  205.  
  206. // SES does not allow sending messages from an unverified address,
  207. // so replace the message's "From:" header with the original
  208. // recipient (which is a verified domain)
  209. header = header.replace(
  210. /^From: (.*(?:\r?\n\s+.*)*)/mg,
  211. function(match, from) {
  212. var fromText;
  213. if (data.config.fromEmail) {
  214. fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
  215. ' <' + data.config.fromEmail + '>';
  216. } else {
  217. fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
  218. ' <' + data.originalRecipient + '>';
  219. }
  220. return fromText;
  221. });
  222.  
  223. // Add a prefix to the Subject
  224. if (data.config.subjectPrefix) {
  225. header = header.replace(
  226. /^Subject: (.*)/mg,
  227. function(match, subject) {
  228. return 'Subject: ' + data.config.subjectPrefix + subject;
  229. });
  230. }
  231.  
  232. // Replace original 'To' header with a manually defined one
  233. if (data.config.toEmail) {
  234. header = header.replace(/^To: (.*)/mg, () => 'To: ' + data.config.toEmail);
  235. }
  236.  
  237. // Remove the Return-Path header.
  238. header = header.replace(/^Return-Path: (.*)\r?\n/mg, '');
  239.  
  240. // Remove Sender header.
  241. header = header.replace(/^Sender: (.*)\r?\n/mg, '');
  242.  
  243. // Remove Message-ID header.
  244. header = header.replace(/^Message-ID: (.*)\r?\n/mig, '');
  245.  
  246. // Remove all DKIM-Signature headers to prevent triggering an
  247. // "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
  248. // These signatures will likely be invalid anyways, since the From
  249. // header was modified.
  250. header = header.replace(/^DKIM-Signature: .*\r?\n(\s+.*\r?\n)*/mg, '');
  251.  
  252. data.emailData = header + body;
  253. return Promise.resolve(data);
  254. };
  255.  
  256. /**
  257. * Send email using the SES sendRawEmail command.
  258. *
  259. * @param {object} data - Data bundle with context, email, etc.
  260. *
  261. * @return {object} - Promise resolved with data.
  262. */
  263. exports.sendMessage = function(data) {
  264. var promises = [];
  265. for (var i = 0; i < data.recipients.length; i += 50) {
  266. var params = {
  267. Destinations: data.recipients.slice(i, i + 50),
  268. Source: data.originalRecipient,
  269. RawMessage: {
  270. Data: data.emailData
  271. }
  272. };
  273. data.log({level: "info", message: "sendMessage: Sending email via SES. " +
  274. "Original recipients: " + data.originalRecipients.join(", ") +
  275. ". Transformed recipients: " + params.Destinations.join(", ") + "."});
  276. promises.push(new Promise(function(resolve, reject) {
  277. data.ses.sendRawEmail(params, function(err, result) {
  278. if (err) {
  279. data.log({level: "error", message: "sendRawEmail() returned error.",
  280. error: err, stack: err.stack});
  281. return reject(new Error('Error: Email sending failed.'));
  282. }
  283. data.log({level: "info", message: "sendRawEmail() successful.",
  284. result: result});
  285. resolve(data);
  286. });
  287. }));
  288. }
  289.  
  290. return Promise.all(promises);
  291. };
  292.  
  293. /**
  294. * Handler function to be invoked by AWS Lambda with an inbound SES email as
  295. * the event.
  296. *
  297. * @param {object} event - Lambda event from inbound email received by AWS SES.
  298. * @param {object} context - Lambda context object.
  299. * @param {object} callback - Lambda callback object.
  300. * @param {object} overrides - Overrides for the default data, including the
  301. * configuration, SES object, and S3 object.
  302. */
  303. exports.handler = function(event, context, callback, overrides) {
  304. var steps = overrides && overrides.steps ? overrides.steps :
  305. [
  306. exports.parseEvent,
  307. exports.transformRecipients,
  308. exports.fetchMessage,
  309. exports.processMessage,
  310. exports.sendMessage
  311. ];
  312. var data = {
  313. event: event,
  314. callback: callback,
  315. context: context,
  316. config: overrides && overrides.config ? overrides.config : defaultConfig,
  317. log: overrides && overrides.log ? overrides.log : console.log,
  318. ses: overrides && overrides.ses ? overrides.ses : new AWS.SES(),
  319. s3: overrides && overrides.s3 ?
  320. overrides.s3 : new AWS.S3({signatureVersion: 'v4'})
  321. };
  322. Promise.series(steps, data)
  323. .then(function(data) {
  324. data.log({level: "info", message: "Process finished successfully."});
  325. return data.callback();
  326. })
  327. .catch(function(err) {
  328. data.log({level: "error", message: "Step returned error: " + err.message,
  329. error: err, stack: err.stack});
  330. return data.callback(new Error("Error: Step returned error."));
  331. });
  332. };
  333.  
  334. Promise.series = function(promises, initValue) {
  335. return promises.reduce(function(chain, promise) {
  336. if (typeof promise !== 'function') {
  337. return Promise.reject(new Error("Error: Invalid promise item: " +
  338. promise));
  339. }
  340. return chain.then(promise);
  341. }, Promise.resolve(initValue));
  342. };
Add Comment
Please, Sign In to add comment