diff --git a/packages/inbound-email-handler/package.json b/packages/inbound-email-handler/package.json index 405388785..b8a2e4f78 100644 --- a/packages/inbound-email-handler/package.json +++ b/packages/inbound-email-handler/package.json @@ -24,6 +24,7 @@ "@types/json-bigint": "^1.0.1", "@types/node": "^14.11.2", "@types/rfc2047": "^2.0.1", + "@types/showdown": "^2.0.1", "chai": "^4.3.6", "eslint-plugin-prettier": "^4.0.0", "mocha": "^10.0.0" @@ -39,6 +40,7 @@ "jsonwebtoken": "^8.5.1", "parse-headers": "^2.0.4", "parse-multipart-data": "^1.2.1", - "rfc2047": "^4.0.1" + "rfc2047": "^4.0.1", + "showdown": "^2.1.0" } } diff --git a/packages/inbound-email-handler/src/index.ts b/packages/inbound-email-handler/src/index.ts index 1555b41de..4a0f2f0a7 100644 --- a/packages/inbound-email-handler/src/index.ts +++ b/packages/inbound-email-handler/src/index.ts @@ -12,6 +12,7 @@ import * as jwt from 'jsonwebtoken' import parseHeaders from 'parse-headers' import * as multipart from 'parse-multipart-data' import rfc2047 from 'rfc2047' +import { Converter } from 'showdown' import { promisify } from 'util' import { Attachment, handleAttachments, isAttachment } from './attachment' import { @@ -36,6 +37,11 @@ const signToken = promisify(jwt.sign) const NEWSLETTER_EMAIL_RECEIVED_TOPIC = 'newsletterEmailReceived' const NON_NEWSLETTER_EMAIL_TOPIC = 'nonNewsletterEmailReceived' const pubsub = new PubSub() +const converter = new Converter() + +export const plainTextToHtml = (text: string): string => { + return converter.makeHtml(text) +} export const publishMessage = async ( topic: string, @@ -163,19 +169,24 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction( await handleAttachments(to, subject, attachments, receivedEmailId) return res.send('ok') } + + // convert text to html if html is not available + const content = html || plainTextToHtml(text) + // all other emails are considered newsletters const newsletterMessage = await handleNewsletter({ from, to, subject, - html, + html: content, headers, }) + // queue newsletter emails await pubsub.topic(NEWSLETTER_EMAIL_RECEIVED_TOPIC).publishMessage({ json: { email: to, - content: html || text, // html is preferred + content, url: generateUniqueUrl(), title: subject, author: parseAuthor(from), diff --git a/packages/inbound-email-handler/test/newsletter.test.ts b/packages/inbound-email-handler/test/newsletter.test.ts index b32a7f504..cf9bff45c 100644 --- a/packages/inbound-email-handler/test/newsletter.test.ts +++ b/packages/inbound-email-handler/test/newsletter.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import 'mocha' import parseHeaders from 'parse-headers' import rfc2047 from 'rfc2047' -import { parsedTo } from '../src' +import { parsedTo, plainTextToHtml } from '../src' import { getConfirmationCode, isGoogleConfirmationEmail, @@ -138,3 +138,29 @@ describe('decode and parse headers', () => { }) }) }) + +describe('plainTextToHtml', () => { + it('converts text to html', () => { + const text = + 'DEVOPS WEEKLY\r\n' + + 'ISSUE #665 - 24th September 2023\r\n' + + '\r\n' + + 'A few posts on CI tooling this week, along with a good introduction to developer portals/platforms and other topics.\r\n' + + '\r\n' + + 'StackHawk sponsors Devops Weekly\r\n' + + '============================\r\n' + + '\r\n' + + 'Experience automated security testing without the hassle of connecting your own app or configuring an environment! Follow the Tutorial to try out StackHawk and explore a world where security becomes an accelerator, not a blocker\r\n' + + '\r\n' + + 'https://sthwk.com/tutorial\r\n' + + '\r\n' + expect(plainTextToHtml(text)).to.eql( + `
DEVOPS WEEKLY +ISSUE #665 - 24th September 2023
+A few posts on CI tooling this week, along with a good introduction to developer portals/platforms and other topics.
+Experience automated security testing without the hassle of connecting your own app or configuring an environment! Follow the Tutorial to try out StackHawk and explore a world where security becomes an accelerator, not a blocker
+https://sthwk.com/tutorial
` + ) + }) +}) diff --git a/yarn.lock b/yarn.lock index 9a8497501..a6626e631 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7985,6 +7985,11 @@ "@types/mime" "^1" "@types/node" "*" +"@types/showdown@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/showdown/-/showdown-2.0.1.tgz#24134738ba3107237d6a783e054a54773e739f81" + integrity sha512-xdnAw2nFqomkaL0QdtEk0t7yz26UkaVPl4v1pYJvtE1T0fmfQEH3JaxErEhGByEAl3zUZrkNBlneuJp0WJGqEA== + "@types/sinon-chai@^3.2.8": version "3.2.8" resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.8.tgz#5871d09ab50d671d8e6dd72e9073f8e738ac61dc" @@ -11214,7 +11219,7 @@ commander@^8.3.0: resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@^9.1.0: +commander@^9.0.0, commander@^9.1.0: version "9.5.0" resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== @@ -24526,6 +24531,13 @@ shimmer@^1.2.1: resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== +showdown@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-2.1.0.tgz#1251f5ed8f773f0c0c7bfc8e6fd23581f9e545c5" + integrity sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ== + dependencies: + commander "^9.0.0" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"