From 634c16a01997ef4497f0c12e980b532cfb04c544 Mon Sep 17 00:00:00 2001 From: Afonso Coutinho <116525378+afonsopc@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:42:20 +0100 Subject: [PATCH] fix: reply-all Cc's the user's own other addresses (multi-account) (#672) * feat: publish all configured email addresses for reply-all exclusion * fix: exclude all of the user's own addresses from reply-all, not just the active one * test: reply-all excludes all of the user's configured addresses --- static/js/emailInbox.js | 12 +++++++----- static/js/emailLibrary.js | 9 +++++++++ static/js/emailLibrary/replyRecipients.js | 16 +++++++++------- tests/test_reply_recipients_js.py | 13 +++++++++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/static/js/emailInbox.js b/static/js/emailInbox.js index 762fb44..8ca1a6a 100644 --- a/static/js/emailInbox.js +++ b/static/js/emailInbox.js @@ -722,10 +722,12 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') { em.is_read = true; if (itemEl) itemEl.classList.remove('email-unread'); - // Get my own address to exclude from Reply All. window._myEmailAddress - // is populated from the configured account on init; the empty fallback - // simply means "no exclusion" — better than baking in a real address. - const myAddress = (window._myEmailAddress || '').toLowerCase(); + // Addresses to exclude from Reply All. Prefer the full set of configured + // accounts (so a multi-account user's other mailboxes are excluded too), + // falling back to the single active address. Empty ⇒ no exclusion. + const myAddresses = (Array.isArray(window._myEmailAddresses) && window._myEmailAddresses.length) + ? window._myEmailAddresses + : (window._myEmailAddress ? [window._myEmailAddress] : []); let toAddress = data.from_address; let ccAddresses = ''; @@ -733,7 +735,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') { if (mode === 'reply-all') { // Build reply-all: TO = original sender, CC = everyone else (To + Cc minus me) - ccAddresses = buildReplyAllCc(data, myAddress); + ccAddresses = buildReplyAllCc(data, myAddresses); } else if (mode === 'forward') { toAddress = ''; subjectPrefix = 'Fwd: '; diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index 8817554..781e3a5 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -532,6 +532,15 @@ function _publishActiveAccount() { || accts.find(a => a && a.is_default) || accts[0]; window._myEmailAddress = (active && (active.from_address || active.imap_user)) || ''; + // Also publish every configured address so reply-all can exclude all of + // the user's own mailboxes, not just the active one (multi-account users + // were getting their other addresses added to Cc). + const all = []; + for (const a of accts) { + if (a && a.from_address) all.push(a.from_address); + if (a && a.imap_user) all.push(a.imap_user); + } + window._myEmailAddresses = all; } catch (_) {} } diff --git a/static/js/emailLibrary/replyRecipients.js b/static/js/emailLibrary/replyRecipients.js index 89f0341..6a5bf22 100644 --- a/static/js/emailLibrary/replyRecipients.js +++ b/static/js/emailLibrary/replyRecipients.js @@ -12,14 +12,16 @@ export function extractEmail(addr) { // Reply-all CC = everyone on the original To + Cc, minus ourselves, with the // original "Name " form preserved. // -// `myAddress` empty/unknown ⇒ no exclusion. Comparing by exact extracted email -// (not a substring `includes`) is what fixes issue #360: an empty self address -// made `"...".includes("")` true for every recipient, so reply-all dropped the -// entire Cc list and kept only the original sender. -export function buildReplyAllCc(data, myAddress) { - const me = (myAddress || '').toLowerCase(); +// `mine` is a single address or a list of the user's own addresses (a +// multi-account user has more than one). Empty/unknown ⇒ no exclusion. +// Comparing by exact extracted email (not a substring `includes`) is what +// fixes issue #360: an empty self address made `"...".includes("")` true for +// every recipient, so reply-all dropped the entire Cc list. +export function buildReplyAllCc(data, mine) { + const list = Array.isArray(mine) ? mine : [mine]; + const me = new Set(list.map((a) => (a || '').toLowerCase()).filter(Boolean)); const split = (s) => (s || '').split(',').map((x) => x.trim()).filter(Boolean); return [...split(data && data.to), ...split(data && data.cc)] - .filter((addr) => !me || extractEmail(addr) !== me) + .filter((addr) => !me.has(extractEmail(addr))) .join(', '); } diff --git a/tests/test_reply_recipients_js.py b/tests/test_reply_recipients_js.py index 77dcc97..e7d5fdf 100644 --- a/tests/test_reply_recipients_js.py +++ b/tests/test_reply_recipients_js.py @@ -51,3 +51,16 @@ def test_reply_all_excludes_only_self_exactly(): cc = json.loads(_run(js)) # Our own address is dropped; a substring-similar address is kept. assert cc == "Alice , bob@x.com" + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_reply_all_excludes_all_of_my_addresses(): + # Multi-account user: every one of their own addresses must be excluded, + # not just the active one. + data = {"to": "Alice , me@work.com", "cc": "me@personal.com, bob@x.com"} + js = f""" + import {{ buildReplyAllCc }} from '{_HELPER.as_posix()}'; + console.log(JSON.stringify(buildReplyAllCc({json.dumps(data)}, ["me@work.com", "me@personal.com"]))); + """ + cc = json.loads(_run(js)) + assert cc == "Alice , bob@x.com"