From 02f4ce81e3ef8fe1cd1478ea827ab0bb3bc0d459 Mon Sep 17 00:00:00 2001 From: Matthieu MALVACHE Date: Wed, 25 Feb 2026 12:35:39 +0100 Subject: [PATCH] fix: include sender display name in From header and default identity selection - Emails now include the identity display name in the From field so recipients see "Name " instead of bare "" - Primary identity (matching login username) is pre-selected in composer dropdown --- ROADMAP.md | 2 ++ app/[locale]/page.tsx | 15 +++++++++++++-- components/email/email-composer.tsx | 5 ++++- lib/jmap/client.ts | 12 +++++++----- stores/auth-store.ts | 7 ++++++- stores/email-store.ts | 6 +++--- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d912eefc..b1fa6ea6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -116,6 +116,8 @@ This document tracks the development status and planned features for JMAP Webmai - [x] Per-identity signatures - [x] Identity badges in email viewer and list - [x] Tag suggestions based on context +- [x] Display name included in From header (recipients see name, not just email) +- [x] Primary identity (matching login) selected by default in composer ### Address Book & Contacts - [x] Contact store with JMAP sync and local fallback diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 75fb33bb..3809381e 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -14,6 +14,7 @@ import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal"; import { useEmailStore } from "@/stores/email-store"; import { useAuthStore } from "@/stores/auth-store"; import { useSettingsStore } from "@/stores/settings-store"; +import { useIdentityStore } from "@/stores/identity-store"; import { useUIStore } from "@/stores/ui-store"; import { useDeviceDetection } from "@/hooks/use-media-query"; import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; @@ -48,6 +49,7 @@ export default function Home() { const [isLoadingConversation, setIsLoadingConversation] = useState(false); const markAsReadTimeoutRef = useRef(null); const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading } = useAuthStore(); + const { identities } = useIdentityStore(); // Mobile/tablet responsive hooks const { isMobile, isTablet } = useDeviceDetection(); @@ -373,12 +375,13 @@ export default function Home() { body: string; draftId?: string; fromEmail?: string; + fromName?: string; identityId?: string; }) => { if (!client) return; try { - await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId); + await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName); setShowComposer(false); // Refresh the current mailbox to update the UI @@ -605,12 +608,20 @@ export default function Home() { throw new Error("No sender email found"); } + const primaryIdentity = identities[0]; + // Send reply with just the body text await sendEmail( client, [sender.email], `Re: ${selectedEmail.subject || "(no subject)"}`, - body + body, + undefined, + undefined, + primaryIdentity?.id, + primaryIdentity?.email, + undefined, + primaryIdentity?.name || undefined ); // Refresh emails to show the sent reply diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 53e8b0a1..a72ba706 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -30,6 +30,7 @@ interface EmailComposerProps { body: string; draftId?: string; fromEmail?: string; + fromName?: string; identityId?: string; }) => void | Promise; onClose?: () => void; @@ -368,7 +369,8 @@ export function EmailComposer({ currentIdentity?.id, fromEmail, draftId || undefined, - uploadedAttachments + uploadedAttachments, + currentIdentity?.name || undefined ); setDraftId(savedDraftId); @@ -484,6 +486,7 @@ export function EmailComposer({ body, draftId: finalDraftId || undefined, fromEmail, + fromName: currentIdentity?.name || undefined, identityId: currentIdentity?.id, }); diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 5ddaa42c..cd7d2d11 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -1242,7 +1242,8 @@ export class JMAPClient { identityId?: string, fromEmail?: string, draftId?: string, - attachments?: Array<{ blobId: string; name: string; type: string; size: number }> + attachments?: Array<{ blobId: string; name: string; type: string; size: number }>, + fromName?: string ): Promise { // Find the drafts mailbox const mailboxes = await this.getMailboxes(); @@ -1256,7 +1257,7 @@ export class JMAPClient { // Build email object with attachments if provided interface EmailDraft { - from: { email: string }[]; + from: { name?: string; email: string }[]; to: { email: string }[]; cc?: { email: string }[]; bcc?: { email: string }[]; @@ -1268,7 +1269,7 @@ export class JMAPClient { attachments?: { blobId: string; type: string; name: string; disposition: string }[]; } const emailData: EmailDraft = { - from: [{ email: fromEmail || this.username }], + from: [{ ...(fromName ? { name: fromName } : {}), email: fromEmail || this.username }], to: to.map(email => ({ email })), cc: cc?.map(email => ({ email })), bcc: bcc?.map(email => ({ email })), @@ -1359,7 +1360,8 @@ export class JMAPClient { bcc?: string[], identityId?: string, fromEmail?: string, - draftId?: string + draftId?: string, + fromName?: string ): Promise { const emailId = draftId || `draft-${Date.now()}`; @@ -1423,7 +1425,7 @@ export class JMAPClient { accountId: this.accountId, create: { [emailId]: { - from: [{ email: fromEmail || this.username }], + from: [{ ...(fromName ? { name: fromName } : {}), email: fromEmail || this.username }], to: to.map(email => ({ email })), cc: cc?.map(email => ({ email })), bcc: bcc?.map(email => ({ email })), diff --git a/stores/auth-store.ts b/stores/auth-store.ts index ca7903dc..e04c7332 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -46,7 +46,12 @@ export const useAuthStore = create()( const client = new JMAPClient(serverUrl, username, effectivePassword); await client.connect(); - const identities = await client.getIdentities(); + const rawIdentities = await client.getIdentities(); + const identities = [...rawIdentities].sort((a, b) => { + const aMatch = a.email === username ? -1 : 0; + const bMatch = b.email === username ? -1 : 0; + return aMatch - bMatch; + }); const primaryIdentity = identities.length > 0 ? identities[0] : null; useIdentityStore.getState().setIdentities(identities); diff --git a/stores/email-store.ts b/stores/email-store.ts index e3b992bc..9084068d 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -53,7 +53,7 @@ interface EmailStore { loadMoreEmails: (client: JMAPClient) => Promise; fetchEmailContent: (client: JMAPClient, emailId: string) => Promise; fetchQuota: (client: JMAPClient) => Promise; - sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string) => Promise; + sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string) => Promise; deleteEmail: (client: JMAPClient, emailId: string) => Promise; markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise; moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise; @@ -314,10 +314,10 @@ export const useEmailStore = create((set, get) => ({ } }, - sendEmail: async (client, to, subject, body, cc, bcc, identityId, fromEmail, draftId) => { + sendEmail: async (client, to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName) => { set({ isLoading: true, error: null }); try { - await client.sendEmail(to, subject, body, cc, bcc, identityId, fromEmail, draftId); + await client.sendEmail(to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName); // Refresh handled by UI layer for immediate feedback set({ isLoading: false }); } catch (error) {