From cea1c856f7887238670bb9cc8c041b0814362434 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Mon, 25 Jul 2022 17:15:12 +0800 Subject: [PATCH] fix not getting unsubscribe email address from beehiv newsletter by decoding the list-unsubscribe header --- packages/inbound-email-handler/package.json | 4 ++- packages/inbound-email-handler/src/index.ts | 27 ++++++++----------- .../inbound-email-handler/src/newsletter.ts | 6 +++-- .../test/newsletter.test.ts | 10 +++++++ yarn.lock | 17 ++++++++++++ 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/packages/inbound-email-handler/package.json b/packages/inbound-email-handler/package.json index 1290b22cd..f9217e172 100644 --- a/packages/inbound-email-handler/package.json +++ b/packages/inbound-email-handler/package.json @@ -23,6 +23,7 @@ "@types/addressparser": "^1.0.1", "@types/json-bigint": "^1.0.1", "@types/node": "^14.11.2", + "@types/rfc2047": "^2.0.1", "eslint-plugin-prettier": "^4.0.0" }, "dependencies": { @@ -34,6 +35,7 @@ "axios": "^0.27.2", "jsonwebtoken": "^8.5.1", "parse-headers": "^2.0.4", - "parse-multipart-data": "^1.2.1" + "parse-multipart-data": "^1.2.1", + "rfc2047": "^4.0.1" } } diff --git a/packages/inbound-email-handler/src/index.ts b/packages/inbound-email-handler/src/index.ts index cbb0d7153..2e1d9022f 100644 --- a/packages/inbound-email-handler/src/index.ts +++ b/packages/inbound-email-handler/src/index.ts @@ -65,29 +65,24 @@ export const inboundEmailHandler = Sentry.GCPFunction.wrapHttpFunction( console.log('headers: ', headers) try { - const from = parsed.from.toString() + const from = parsed.from const subject = parsed.subject const html = parsed.html const text = parsed.text const forwardedAddress = headers['x-forwarded-to'] - const recipientAddress = forwardedAddress - ? forwardedAddress.toString() - : parsed.to - const postHeader = headers['list-post'] - ? headers['list-post'].toString() - : '' - const unSubHeader = headers['list-unsubscribe'] - ? headers['list-unsubscribe'].toString() - : '' + const recipientAddress = forwardedAddress?.toString() || parsed.to + const postHeader = headers['list-post']?.toString() + const unSubHeader = headers['list-unsubscribe'].toString() - // check if it is a forwarding confirmation email or newsletter - const newsletterHandler = getNewsletterHandler( - postHeader, - from, - unSubHeader - ) try { + // check if it is a forwarding confirmation email or newsletter + const newsletterHandler = getNewsletterHandler( + postHeader, + from, + unSubHeader + ) + if (newsletterHandler) { console.log('handleNewsletter', from, recipientAddress) await newsletterHandler.handleNewsletter( diff --git a/packages/inbound-email-handler/src/newsletter.ts b/packages/inbound-email-handler/src/newsletter.ts index 20ff4f867..c3ff83972 100644 --- a/packages/inbound-email-handler/src/newsletter.ts +++ b/packages/inbound-email-handler/src/newsletter.ts @@ -1,6 +1,7 @@ import { PubSub } from '@google-cloud/pubsub' import { v4 as uuidv4 } from 'uuid' import addressparser from 'addressparser' +import rfc2047 from 'rfc2047' interface Unsubscribe { mailTo?: string @@ -20,9 +21,10 @@ const UNSUBSCRIBE_MAIL_TO_PATTERN = /]*)>/ export const parseUnsubscribe = (unSubHeader: string): Unsubscribe => { // parse list-unsubscribe header // e.g. List-Unsubscribe: , + const decoded = rfc2047.decode(unSubHeader) return { - mailTo: unSubHeader.match(UNSUBSCRIBE_MAIL_TO_PATTERN)?.[1], - httpUrl: unSubHeader.match(UNSUBSCRIBE_HTTP_URL_PATTERN)?.[1], + mailTo: decoded.match(UNSUBSCRIBE_MAIL_TO_PATTERN)?.[1], + httpUrl: decoded.match(UNSUBSCRIBE_HTTP_URL_PATTERN)?.[1], } } diff --git a/packages/inbound-email-handler/test/newsletter.test.ts b/packages/inbound-email-handler/test/newsletter.test.ts index 2d0ef1bea..58ffe2ec7 100644 --- a/packages/inbound-email-handler/test/newsletter.test.ts +++ b/packages/inbound-email-handler/test/newsletter.test.ts @@ -167,5 +167,15 @@ describe('Newsletter email test', () => { expect(parseUnsubscribe(header).httpUrl).to.equal(httpUrl) }) + + context('when unsubscribe header rfc2047 encoded', () => { + it('returns mail to address if exists', () => { + const header = `=?us-ascii?Q?=3Cmailto=3A654e9594-184c-4884-8e02-e6e58a3a6871+87e39b3d-c3ca-4be?= =?us-ascii?Q?b-ba4d-977cc2ba61e7+067a353f-f775-4f2c-?= =?us-ascii?Q?a5cc-978df38deeca=40unsub=2Ebeehiiv=2Ecom=3E=2C?= =?us-ascii?Q?_=3Chttps=3A=2F=2Fwww=2Emilkroad=2Ecom=2Fsubscribe=2F87e39b3d-c3ca-4beb-ba4d-97?= =?us-ascii?Q?7cc2ba61e7=2Fmanage=3Fpost=5Fid=3D067a353f-f775?= =?us-ascii?Q?-4f2c-a5cc-978df38deeca=3E?=',` + + expect(parseUnsubscribe(header).mailTo).to.equal( + '654e9594-184c-4884-8e02-e6e58a3a6871+87e39b3d-c3ca-4beb-ba4d-977cc2ba61e7+067a353f-f775-4f2c-a5cc-978df38deeca@unsub.beehiiv.com' + ) + }) + }) }) }) diff --git a/yarn.lock b/yarn.lock index 96f5c31b5..57444b9a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8050,6 +8050,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== +"@types/rfc2047@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/rfc2047/-/rfc2047-2.0.1.tgz#42eb43c161a52fd404a935f032360880ec84d400" + integrity sha512-slgtykv+XXME7EperkdqfdBBUGcs28ru+a21BK0zOQY4IoxE7tEqvIcvAFAz5DJVxyOmoAUXo30Oxpm3KS+TBQ== + "@types/sanitize-html@^1.27.1": version "1.27.2" resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.27.2.tgz#f7bf16ca4b1408278f97ae737f0377a08a10b35c" @@ -15376,6 +15381,11 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.5.tgz#9c574b70c30d615859f2064d2be4335ad6b1a8d6" + integrity sha512-LQ4GtDkFagYaac8u4rE73zWu7h0OUUmR0qVBOgzLyFSoJhoDG2xV9PZJWWyVVcYha/9/RZzQHUinFMbNKiOoAA== + iconv-lite@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" @@ -22038,6 +22048,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfc2047@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/rfc2047/-/rfc2047-4.0.1.tgz#4e1cc217654728f09bc818b53a67d1d9c9d5067c" + integrity sha512-x5zHBAZtSSZDuBNAqGEAVpsQFV+YUluIkMWVaYRMEeGoLPxNVMmg67TxRnXwmRmCB7QaneyrkWXeKqbjfcK6RA== + dependencies: + iconv-lite "0.4.5" + rfdc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"