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
This commit is contained in:
@@ -722,10 +722,12 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') {
|
|||||||
em.is_read = true;
|
em.is_read = true;
|
||||||
if (itemEl) itemEl.classList.remove('email-unread');
|
if (itemEl) itemEl.classList.remove('email-unread');
|
||||||
|
|
||||||
// Get my own address to exclude from Reply All. window._myEmailAddress
|
// Addresses to exclude from Reply All. Prefer the full set of configured
|
||||||
// is populated from the configured account on init; the empty fallback
|
// accounts (so a multi-account user's other mailboxes are excluded too),
|
||||||
// simply means "no exclusion" — better than baking in a real address.
|
// falling back to the single active address. Empty ⇒ no exclusion.
|
||||||
const myAddress = (window._myEmailAddress || '').toLowerCase();
|
const myAddresses = (Array.isArray(window._myEmailAddresses) && window._myEmailAddresses.length)
|
||||||
|
? window._myEmailAddresses
|
||||||
|
: (window._myEmailAddress ? [window._myEmailAddress] : []);
|
||||||
|
|
||||||
let toAddress = data.from_address;
|
let toAddress = data.from_address;
|
||||||
let ccAddresses = '';
|
let ccAddresses = '';
|
||||||
@@ -733,7 +735,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply') {
|
|||||||
|
|
||||||
if (mode === 'reply-all') {
|
if (mode === 'reply-all') {
|
||||||
// Build reply-all: TO = original sender, CC = everyone else (To + Cc minus me)
|
// 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') {
|
} else if (mode === 'forward') {
|
||||||
toAddress = '';
|
toAddress = '';
|
||||||
subjectPrefix = 'Fwd: ';
|
subjectPrefix = 'Fwd: ';
|
||||||
|
|||||||
@@ -532,6 +532,15 @@ function _publishActiveAccount() {
|
|||||||
|| accts.find(a => a && a.is_default)
|
|| accts.find(a => a && a.is_default)
|
||||||
|| accts[0];
|
|| accts[0];
|
||||||
window._myEmailAddress = (active && (active.from_address || active.imap_user)) || '';
|
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 (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ export function extractEmail(addr) {
|
|||||||
// Reply-all CC = everyone on the original To + Cc, minus ourselves, with the
|
// Reply-all CC = everyone on the original To + Cc, minus ourselves, with the
|
||||||
// original "Name <email>" form preserved.
|
// original "Name <email>" form preserved.
|
||||||
//
|
//
|
||||||
// `myAddress` empty/unknown ⇒ no exclusion. Comparing by exact extracted email
|
// `mine` is a single address or a list of the user's own addresses (a
|
||||||
// (not a substring `includes`) is what fixes issue #360: an empty self address
|
// multi-account user has more than one). Empty/unknown ⇒ no exclusion.
|
||||||
// made `"...".includes("")` true for every recipient, so reply-all dropped the
|
// Comparing by exact extracted email (not a substring `includes`) is what
|
||||||
// entire Cc list and kept only the original sender.
|
// fixes issue #360: an empty self address made `"...".includes("")` true for
|
||||||
export function buildReplyAllCc(data, myAddress) {
|
// every recipient, so reply-all dropped the entire Cc list.
|
||||||
const me = (myAddress || '').toLowerCase();
|
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);
|
const split = (s) => (s || '').split(',').map((x) => x.trim()).filter(Boolean);
|
||||||
return [...split(data && data.to), ...split(data && data.cc)]
|
return [...split(data && data.to), ...split(data && data.cc)]
|
||||||
.filter((addr) => !me || extractEmail(addr) !== me)
|
.filter((addr) => !me.has(extractEmail(addr)))
|
||||||
.join(', ');
|
.join(', ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,3 +51,16 @@ def test_reply_all_excludes_only_self_exactly():
|
|||||||
cc = json.loads(_run(js))
|
cc = json.loads(_run(js))
|
||||||
# Our own address is dropped; a substring-similar address is kept.
|
# Our own address is dropped; a substring-similar address is kept.
|
||||||
assert cc == "Alice <alice@x.com>, bob@x.com"
|
assert cc == "Alice <alice@x.com>, 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 <alice@x.com>, 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 <alice@x.com>, bob@x.com"
|
||||||
|
|||||||
Reference in New Issue
Block a user