From ddd0c170c8b6fa902992ed097e80dcd43fb74dbb Mon Sep 17 00:00:00 2001 From: Enes Yilmaz Date: Sat, 8 Aug 2026 22:36:35 -0400 Subject: [PATCH] fix: stop the unsubscribe guard from blocking every real signature The extension found the signature and then refused to delete it. Reported from a live Gmail compose window: loaded, enabled, correct selector, nothing removed. removeBlock() skipped any block whose text matched /unsubscribe|opt.out|.../ when keepUnsubscribe was on, which is the default. Signature variants 17 and 18 both render an "Unsubscribe" or "Opt out" link into every ordinary email, so the text match fired on essentially every signature Mailsuite produces. The guard meant to protect bulk mail was quietly disabling the entire extension. Captured markup from the live compose window shows why the text was never the right thing to look at: Opt out That href is the unsubstituted campaign placeholder. Mailsuite only fills it in when a Campaign actually goes out, so on a one-to-one email it is a dead link wearing the word "Opt out". hasLiveUnsubscribe() now checks whether the link goes anywhere: an anchor with the placeholder href is ignored, a real unsubscribe URL still protects the block. Campaign mail keeps its legally required link, ordinary mail loses the advert. Three tests from the captured DOM, verbatim including the timestamp cell and the Remove button: the live v18 signature is removed under default settings, the placeholder is not mistaken for a live link, and a substituted real unsubscribe URL is still kept. 19 tests, all passing. Version 0.2.1. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013rTepGiDUe15iPn6XqmKjy --- manifest.json | 4 ++-- src/detect.js | 31 +++++++++++++++++++++++++- test/detect.test.js | 54 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 9843c62..7bc945c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ -{ +{ "manifest_version": 3, "name": "Mailsuite Signature Stripper", - "version": "0.2.0", + "version": "0.2.1", "description": "Removes the Mailsuite/Mailtrack promotional signature from your outgoing Gmail messages. Read tracking keeps working.", "permissions": ["storage"], "host_permissions": ["https://mail.google.com/*"], diff --git a/src/detect.js b/src/detect.js index 76f1112..f7577fe 100644 --- a/src/detect.js +++ b/src/detect.js @@ -60,6 +60,33 @@ const UNSUB = /unsubscribe|opt[-\s]?out|darse de baja|se désabonner|abmelden|annulla iscrizione|cancelar subscri|wypisz|取消訂閱|配信停止|수신 거부|सदस्यता समाप्त/i; + /* Mailsuite writes the campaign unsubscribe URL as a placeholder and only + substitutes it when a Campaign actually goes out. An ordinary one-to-one + signature carries the placeholder verbatim: + + Opt out + + which is a dead link, not a recipient's unsubscribe. */ + const CAMPAIGN_PLACEHOLDER = '##mt_unsubscribe_link##'; + + /** + * Whether the block carries a real, working unsubscribe link. + * + * Matching on the words alone was wrong and made the extension useless on the + * two most common signature variants. Both 17 and 18 render an + * "Unsubscribe" / "Opt out" link into every ordinary email, so a text match + * refused to delete almost every signature there is. What matters is whether + * the link actually goes anywhere. + */ + const hasLiveUnsubscribe = (block) => { + for (const a of block.querySelectorAll('a')) { + const href = a.getAttribute('href') || ''; + if (!href || href.includes(CAMPAIGN_PLACEHOLDER)) continue; + if (UNSUB.test(a.textContent || '') || /unsub|opt-?out/i.test(href)) return true; + } + return false; + }; + const PUNCTUATION = /[\s .,;:·|—–()[\]-]/g; /** A link out to Mailsuite's own site, which is what the signature always is. */ @@ -165,7 +192,7 @@ /** Shared teardown: keep the beacon, close the gap, drop the block. */ const removeBlock = (block, root, keepUnsubscribe) => { - if (keepUnsubscribe && UNSUB.test(block.textContent || '')) return false; + if (keepUnsubscribe && hasLiveUnsubscribe(block)) return false; /* Trim first. rescueBeacons parks the pixel immediately before the block, which would otherwise stop trimBefore seeing the blank line above it. */ trimBefore(block); @@ -207,6 +234,8 @@ SIGNATURE_MARKERS, PHRASES, UNSUB, + CAMPAIGN_PLACEHOLDER, + hasLiveUnsubscribe, markedBlocks, promoAnchor, looksLikeBeacon, diff --git a/test/detect.test.js b/test/detect.test.js index 2b03665..bd1d025 100644 --- a/test/detect.test.js +++ b/test/detect.test.js @@ -187,6 +187,60 @@ test('a beacon inside the real signature survives it', () => { assert.equal(root.querySelectorAll('img[src*="/trace/mail/"]').length, 1); }); +/* + * Captured verbatim from a live Gmail compose window, Mailsuite 12.87.0, + * signature version 18. Note the opt-out href is the unsubstituted campaign + * placeholder, so it is a dead link, not a recipient's unsubscribe. + */ +const CAPTURED_V18 = `
+ + + + + + + +
+ Mailsuite + + Sender notified with Mailtrack  ·  Opt out
+
08/08/26, 10:33:25 PM +
+ +
+
+
`; + +test('removes the captured live v18 signature, Opt out link and all', () => { + const root = compose('
testing
' + CAPTURED_V18); + + assert.equal(detect.scrubRoot(root), 1, 'default settings must remove it'); + assert.equal(root.querySelector('#mt-signature'), null); + assert.doesNotMatch(root.textContent, /Sender notified with Mailtrack/); + assert.match(root.textContent, /testing/); +}); + +test('the campaign placeholder is not mistaken for a live unsubscribe', () => { + const root = compose(CAPTURED_V18); + const block = root.querySelector('#mt-signature'); + + assert.equal( + detect.hasLiveUnsubscribe(block), + false, + 'href is the unsubstituted placeholder, so the link goes nowhere', + ); + assert.equal(detect.UNSUB.test(block.textContent), true, 'the words are still there, which is why matching on text was wrong'); +}); + +test('a real campaign unsubscribe is still protected', () => { + const real = CAPTURED_V18.replace('https://##mt_unsubscribe_link##', 'https://mailsuite.com/unsubscribe/9f2a1c'); + const block = compose(real).querySelector('#mt-signature'); + assert.equal(detect.hasLiveUnsubscribe(block), true); + + assert.equal(detect.scrubRoot(compose(real), { keepUnsubscribe: true }), 0, 'kept by default'); + assert.equal(detect.scrubRoot(compose(real), { keepUnsubscribe: false }), 1, 'removed when told to'); +}); + test('catches the signature after Gmail has prefixed the id', () => { /* Gmail rewrites ids when it renders or quotes a message, which is why Mailsuite ships its own un-prefixing helper. */