Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -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/*"],
Expand Down
31 changes: 30 additions & 1 deletion src/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<a href="https://##mt_unsubscribe_link##">Opt out</a>

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. */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -207,6 +234,8 @@
SIGNATURE_MARKERS,
PHRASES,
UNSUB,
CAMPAIGN_PLACEHOLDER,
hasLiveUnsubscribe,
markedBlocks,
promoAnchor,
looksLikeBeacon,
Expand Down
54 changes: 54 additions & 0 deletions test/detect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<div id="mt-signature" contenteditable="false" g_editable="false">
<table border="0" cellpadding="8" cellspacing="0" contenteditable="false" g_editable="false" data-signature-template="senderNotified" data-signature-version="18" style="user-select: none;">
<tbody><tr style="display:flex;">
<td style="padding:0 4px 0 0">
<img src="https://s3.amazonaws.com/mailtrack-signature/logo-grey.png" alt="Mailsuite" class="mt-no-pointer-events" width="24" height="20" g_editable="false">
</td>
<td style="padding:0 10px 0 0">
<span style="color:#333;font-size:12px">Sender notified with Mailtrack &nbsp;·&nbsp; <a href="https://##mt_unsubscribe_link##" target="_blank" style="color:#666">Opt out</a></span><br>
</td>
<td><span style="color: transparent; font-size: 0;">08/08/26, 10:33:25 PM</span></td>
<td class="mt-remove-signature-button-container" style="padding:4px 0 0 0">
<div class="mt-remove-signature-button center" g_editable="false">
<button class="mt-remove" style="display:none;"><span style="color: #E55A50">Remove</span></button>
</div>
</td>
</tr>
</tbody></table>
</div>`;

test('removes the captured live v18 signature, Opt out link and all', () => {
const root = compose('<div>testing</div>' + 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. */
Expand Down
Loading