mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
fix: reply to a message from another connected account goes to that account
With several accounts connected, the composer treated every account's identities as "own" when deciding whether a message was self-sent (#703). A message that account B sent to account A therefore looked self-sent when replying from A, so the reply was addressed to the original To - account A itself - and From was switched to B. Self-sent now means "sent by the account holding the original": buildReplyRecipients takes an optional selfSentEmails list (defaulting to ownEmails, so single-account behaviour is unchanged), and the composer passes the source account's identities there and to the From selection. Reply-all still drops every connected account's addresses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
107d731af7
commit
76dbaf532a
@@ -538,17 +538,25 @@ export function EmailComposer({
|
||||
identities.map(i => i.email),
|
||||
[composerAccount?.email, composerAccount?.username, replyAccount?.email, replyAccount?.username],
|
||||
);
|
||||
const ownIdentityEmailsKey = ownIdentityEmails.map((e) => e.toLowerCase()).join(',');
|
||||
// "Self-sent" means sent by the account holding the original. A message
|
||||
// another connected account sent to this one was received here, and its
|
||||
// reply must go back to that sender, not to this account's own address.
|
||||
const sourceAccount = replyAccount ?? composerAccount;
|
||||
const sourceAccountEmails = ownReplyAddresses(
|
||||
sourceAccountIdentities.map(i => i.email),
|
||||
[sourceAccount?.email, sourceAccount?.username],
|
||||
);
|
||||
const ownIdentityEmailsKey = [...ownIdentityEmails, '|', ...sourceAccountEmails].map((e) => e.toLowerCase()).join(',');
|
||||
|
||||
// Initialize with reply/forward data if provided
|
||||
const getInitialTo = (): Recipient[] => {
|
||||
if (mode !== 'reply' && mode !== 'replyAll') return [];
|
||||
return buildReplyRecipients(replyTo, mode, ownIdentityEmails).to.map(toRecipient);
|
||||
return buildReplyRecipients(replyTo, mode, ownIdentityEmails, sourceAccountEmails).to.map(toRecipient);
|
||||
};
|
||||
|
||||
const getInitialCc = (): Recipient[] => {
|
||||
if (mode !== 'replyAll') return [];
|
||||
return buildReplyRecipients(replyTo, mode, ownIdentityEmails).cc.map(toRecipient);
|
||||
return buildReplyRecipients(replyTo, mode, ownIdentityEmails, sourceAccountEmails).cc.map(toRecipient);
|
||||
};
|
||||
|
||||
const getInitialSubject = () => {
|
||||
@@ -1194,10 +1202,10 @@ export function EmailComposer({
|
||||
// identity that sent it. Resolving from the recipients here would pick the
|
||||
// *other* party's address - and on a catch-all domain it would even set a
|
||||
// From override to their address. An identity on the message's own account
|
||||
// wins when two accounts share the address.
|
||||
if (isSelfSent({ from: replyTo?.from }, identities.map(i => i.email).filter(Boolean))) {
|
||||
const senderIdentityId = findDraftIdentityId(sourceAccountIdentities, replyTo?.from?.[0])
|
||||
?? findDraftIdentityId(identities, replyTo?.from?.[0]);
|
||||
// wins when two accounts share the address. Only the message's own account
|
||||
// counts: mail another connected account sent here is a received message.
|
||||
if (isSelfSent({ from: replyTo?.from }, sourceAccountIdentities.map(i => i.email).filter(Boolean))) {
|
||||
const senderIdentityId = findDraftIdentityId(sourceAccountIdentities, replyTo?.from?.[0]);
|
||||
if (senderIdentityId) {
|
||||
setSelectedIdentityId(senderIdentityId);
|
||||
return;
|
||||
|
||||
@@ -124,6 +124,40 @@ describe('buildReplyRecipients', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('message from another connected account', () => {
|
||||
// Two accounts connected: book@ sent this to mail@, and we reply from mail@.
|
||||
const ALL = ['mail@hotel.example', 'book@hotel.example'];
|
||||
const THIS_ACCOUNT = ['mail@hotel.example'];
|
||||
const fromOtherAccount = {
|
||||
from: [{ email: 'book@hotel.example', name: 'Booking' }],
|
||||
to: [{ email: 'mail@hotel.example' }, { email: 'guest@other.com' }],
|
||||
cc: [{ email: 'book@hotel.example' }],
|
||||
};
|
||||
|
||||
it('replies to the other account, not back to ourselves', () => {
|
||||
const { to, cc } = buildReplyRecipients(fromOtherAccount, 'reply', ALL, THIS_ACCOUNT);
|
||||
expect(emails(to)).toEqual(['book@hotel.example']);
|
||||
expect(cc).toEqual([]);
|
||||
});
|
||||
|
||||
it('reply-all still drops every connected account from the other recipients', () => {
|
||||
const { to, cc } = buildReplyRecipients(fromOtherAccount, 'replyAll', ALL, THIS_ACCOUNT);
|
||||
expect(emails(to)).toEqual(['book@hotel.example', 'guest@other.com']);
|
||||
expect(cc).toEqual([]);
|
||||
});
|
||||
|
||||
it('still continues the thread for a message this account sent', () => {
|
||||
const ownSent = { ...fromOtherAccount, from: [{ email: 'mail@hotel.example' }], to: [{ email: 'guest@other.com' }] };
|
||||
const { to } = buildReplyRecipients(ownSent, 'reply', ALL, THIS_ACCOUNT);
|
||||
expect(emails(to)).toEqual(['guest@other.com']);
|
||||
});
|
||||
|
||||
it('without selfSentEmails every connected account counts as self (single-account behaviour)', () => {
|
||||
const { to } = buildReplyRecipients(fromOtherAccount, 'reply', ALL);
|
||||
expect(emails(to)).toEqual(['mail@hotel.example', 'guest@other.com']);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns nothing without a source message', () => {
|
||||
expect(buildReplyRecipients(undefined, 'replyAll', OWN)).toEqual({ to: [], cc: [] });
|
||||
});
|
||||
|
||||
@@ -95,11 +95,19 @@ export function isSelfSent(source: ReplySource | undefined, ownEmails: string[])
|
||||
*
|
||||
* A self-sent message with no visible recipients (Bcc-only) has nothing to
|
||||
* continue to, so it falls back to the normal behaviour.
|
||||
*
|
||||
* `selfSentEmails` decides what counts as "self-sent" and defaults to
|
||||
* `ownEmails`. With several connected accounts, `ownEmails` holds every
|
||||
* account's addresses (so reply-all still drops all of them), but a message
|
||||
* one of those accounts sent to another is a received message for the
|
||||
* account it landed in: pass only that account's addresses here, or the
|
||||
* reply goes back to the original To - the account itself.
|
||||
*/
|
||||
export function buildReplyRecipients(
|
||||
source: ReplySource | undefined,
|
||||
mode: 'reply' | 'replyAll',
|
||||
ownEmails: string[],
|
||||
selfSentEmails: string[] = ownEmails,
|
||||
): ReplyRecipientsResult {
|
||||
if (!source) return { to: [], cc: [] };
|
||||
|
||||
@@ -108,7 +116,7 @@ export function buildReplyRecipients(
|
||||
const replyToList = withEmail(source.replyToAddresses);
|
||||
const externalReplyTo = replyToList.filter((r) => !isOwnAddress(r.email, ownEmails));
|
||||
|
||||
if (!externalReplyTo.length && isSelfSent(source, ownEmails)) {
|
||||
if (!externalReplyTo.length && isSelfSent(source, selfSentEmails)) {
|
||||
const originalTo = withEmail(source.to);
|
||||
if (originalTo.length > 0) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user