mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: preselect the shared folder identity for new messages
Composing a new message while a shared/group folder was selected started it as the reaching login's primary address instead of the shared one, even when a matching send-as identity existed. `composeFromAccountEmail` was resolved from `getAccountById(viewingAccountId ?? activeAccountId)`. A delegated mailbox has no separate login and no AccountEntry, and `setViewingAccount` is only ever called with `null`, so selecting a shared folder leaves both ids pointing at the primary account: `findComposeIdentityId` was handed the wrong address and correctly matched the primary identity. Resolve the address from the selected mailbox instead, falling back to the account lookup for own folders. `accountName` is the JMAP `Account.name`, which RFC 8620 describes as "e.g., the email address of the account"; a server that puts a human label there produces no identity match and the composer keeps the primary identity, which is the previous behaviour. Replies are unaffected - `composeFromAccountEmail` is only read when `mode === 'compose'`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3560cf258b
commit
a971d5a119
@@ -62,7 +62,7 @@ import { isFilePreviewable } from "@/lib/file-preview";
|
||||
import { appendHtmlSignature, appendPlainTextSignature } from "@/lib/signature-utils";
|
||||
import { computeReplyThreadingHeaders } from "@/lib/email-threading";
|
||||
import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import";
|
||||
import { findDraftIdentityId, resolveReplyFrom, type ReplyFromResolution } from "@/lib/reply-identity";
|
||||
import { findDraftIdentityId, resolveComposeAccountEmail, resolveReplyFrom, type ReplyFromResolution } from "@/lib/reply-identity";
|
||||
import { buildReplyRecipients, isSelfSent } from "@/lib/reply-recipients";
|
||||
import { useProMultiAccountIdentities } from "@/hooks/use-pro-multi-account-identities";
|
||||
import { Search, Filter, ChevronDown, X, Paperclip, Star, Mail, MailOpen, RotateCcw, PenSquare, PenLine, CheckSquare, Square, AlertTriangle } from "lucide-react";
|
||||
@@ -3656,11 +3656,13 @@ export function MailApp({ linkSegments }: MailAppProps = {}) {
|
||||
<EmailComposer
|
||||
key={composerSessionId}
|
||||
mode={pendingDraft?.mode ?? composerMode}
|
||||
composeFromAccountEmail={
|
||||
composeFromAccountEmail={resolveComposeAccountEmail(
|
||||
mailboxes,
|
||||
selectedMailbox,
|
||||
useAccountStore
|
||||
.getState()
|
||||
.getAccountById(viewingAccountId ?? activeAccountId ?? '')?.email
|
||||
}
|
||||
.getAccountById(viewingAccountId ?? activeAccountId ?? '')?.email,
|
||||
)}
|
||||
replyTo={pendingDraft !== null ? pendingDraft.replyTo : (selectedEmail ? {
|
||||
from: selectedEmail.from,
|
||||
replyToAddresses: selectedEmail.replyTo,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { findComposeIdentityId, findDraftIdentityId, findReplyIdentityId, resolveReplyFrom } from '../reply-identity';
|
||||
import { findComposeIdentityId, findDraftIdentityId, findReplyIdentityId, resolveComposeAccountEmail, resolveReplyFrom } from '../reply-identity';
|
||||
import type { Identity } from '../jmap/types';
|
||||
|
||||
const identities: Identity[] = [
|
||||
@@ -137,4 +137,53 @@ describe('resolveReplyFrom', () => {
|
||||
expect(resolveReplyFrom(identities, { to: [{ email: 'nobody@elsewhere.com' }] }))
|
||||
.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveComposeAccountEmail', () => {
|
||||
// Selecting a shared/group folder in the "Shared" sidebar does not move the
|
||||
// active account, so the composer used to preselect the reaching login's
|
||||
// primary identity and start every new message as the wrong sender.
|
||||
const mailboxes = [
|
||||
{ id: 'a-inbox' },
|
||||
{ id: 'owner-x:x-inbox', isShared: true, accountName: 'team@shared.example' },
|
||||
{ id: 'owner-y:y-inbox', isShared: true, accountName: 'Support Team' },
|
||||
];
|
||||
|
||||
it('uses the shared folder owner address when a shared folder is selected', () => {
|
||||
expect(resolveComposeAccountEmail(mailboxes, 'owner-x:x-inbox', 'me@primary.example'))
|
||||
.toBe('team@shared.example');
|
||||
});
|
||||
|
||||
it('keeps the active account address for an own folder', () => {
|
||||
expect(resolveComposeAccountEmail(mailboxes, 'a-inbox', 'me@primary.example'))
|
||||
.toBe('me@primary.example');
|
||||
});
|
||||
|
||||
it('falls back to the active account when the account name is not an address', () => {
|
||||
// RFC 8620 suggests the address for Account.name, but a server may put a
|
||||
// human label there; that must not become a From address.
|
||||
expect(resolveComposeAccountEmail(mailboxes, 'owner-y:y-inbox', 'me@primary.example'))
|
||||
.toBe('me@primary.example');
|
||||
});
|
||||
|
||||
it('handles an unknown or empty selection', () => {
|
||||
expect(resolveComposeAccountEmail(mailboxes, 'nope', 'me@primary.example')).toBe('me@primary.example');
|
||||
expect(resolveComposeAccountEmail(mailboxes, null, 'me@primary.example')).toBe('me@primary.example');
|
||||
expect(resolveComposeAccountEmail(mailboxes, 'owner-x:x-inbox', null)).toBe('team@shared.example');
|
||||
expect(resolveComposeAccountEmail(mailboxes, 'a-inbox', null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preselects the shared identity end-to-end (the reported repro)', () => {
|
||||
// Shared folder selected -> resolved address -> matching send-as identity.
|
||||
const identities: Identity[] = [
|
||||
{ id: 'primary', name: 'D R', email: 'me@primary.example', mayDelete: false },
|
||||
{ id: 'shared', name: 'D R', email: 'team@shared.example', mayDelete: false },
|
||||
];
|
||||
const address = resolveComposeAccountEmail(mailboxes, 'owner-x:x-inbox', 'me@primary.example');
|
||||
expect(findComposeIdentityId(identities, address)).toBe('shared');
|
||||
|
||||
// Same call on an own folder keeps the primary identity.
|
||||
const own = resolveComposeAccountEmail(mailboxes, 'a-inbox', 'me@primary.example');
|
||||
expect(findComposeIdentityId(identities, own)).toBe('primary');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,38 @@ export function findComposeIdentityId(
|
||||
return baseIdentity?.id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address whose identity `findComposeIdentityId` should preselect, based on the
|
||||
* mailbox currently open.
|
||||
*
|
||||
* Selecting a shared/group folder in the "Shared" sidebar section does not move
|
||||
* the active account - a delegated mailbox has no separate login, it is reached
|
||||
* through the viewer's - so the composer fell back to the reaching login's
|
||||
* primary address and started every new message as the wrong sender. The owner
|
||||
* of the selected folder is the correct default there.
|
||||
*
|
||||
* `accountName` is the JMAP `Account.name`, which RFC 8620 describes as "e.g.,
|
||||
* the email address of the account". Servers that put a human label there
|
||||
* instead simply produce no identity match, and the caller keeps the primary
|
||||
* identity - exactly the previous behaviour. The `@` test keeps that intent
|
||||
* explicit rather than relying on the match to fail.
|
||||
*/
|
||||
export function resolveComposeAccountEmail(
|
||||
mailboxes: Array<{ id: string; isShared?: boolean; accountName?: string }>,
|
||||
selectedMailbox?: string | null,
|
||||
activeAccountEmail?: string | null,
|
||||
): string | undefined {
|
||||
const current = selectedMailbox
|
||||
? mailboxes.find((mailbox) => mailbox.id === selectedMailbox)
|
||||
: undefined;
|
||||
|
||||
if (current?.isShared && current.accountName?.includes('@')) {
|
||||
return current.accountName;
|
||||
}
|
||||
|
||||
return activeAccountEmail ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the identity a draft was composed with from its saved From. A draft
|
||||
* stores only the From address+name, not an identityId, so when two identities
|
||||
|
||||
Reference in New Issue
Block a user