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 <email>" instead of bare "<email>"
- Primary identity (matching login username) is pre-selected in composer dropdown
This commit is contained in:
Matthieu MALVACHE
2026-02-25 12:35:39 +01:00
committed by Matthieu MALVACHE
parent ff3ae80f23
commit 02f4ce81e3
6 changed files with 35 additions and 12 deletions
+2
View File
@@ -116,6 +116,8 @@ This document tracks the development status and planned features for JMAP Webmai
- [x] Per-identity signatures - [x] Per-identity signatures
- [x] Identity badges in email viewer and list - [x] Identity badges in email viewer and list
- [x] Tag suggestions based on context - [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 ### Address Book & Contacts
- [x] Contact store with JMAP sync and local fallback - [x] Contact store with JMAP sync and local fallback
+13 -2
View File
@@ -14,6 +14,7 @@ import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal";
import { useEmailStore } from "@/stores/email-store"; import { useEmailStore } from "@/stores/email-store";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
import { useIdentityStore } from "@/stores/identity-store";
import { useUIStore } from "@/stores/ui-store"; import { useUIStore } from "@/stores/ui-store";
import { useDeviceDetection } from "@/hooks/use-media-query"; import { useDeviceDetection } from "@/hooks/use-media-query";
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
@@ -48,6 +49,7 @@ export default function Home() {
const [isLoadingConversation, setIsLoadingConversation] = useState(false); const [isLoadingConversation, setIsLoadingConversation] = useState(false);
const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null); const markAsReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading } = useAuthStore(); const { isAuthenticated, client, logout, checkAuth, isLoading: authLoading } = useAuthStore();
const { identities } = useIdentityStore();
// Mobile/tablet responsive hooks // Mobile/tablet responsive hooks
const { isMobile, isTablet } = useDeviceDetection(); const { isMobile, isTablet } = useDeviceDetection();
@@ -373,12 +375,13 @@ export default function Home() {
body: string; body: string;
draftId?: string; draftId?: string;
fromEmail?: string; fromEmail?: string;
fromName?: string;
identityId?: string; identityId?: string;
}) => { }) => {
if (!client) return; if (!client) return;
try { 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); setShowComposer(false);
// Refresh the current mailbox to update the UI // Refresh the current mailbox to update the UI
@@ -605,12 +608,20 @@ export default function Home() {
throw new Error("No sender email found"); throw new Error("No sender email found");
} }
const primaryIdentity = identities[0];
// Send reply with just the body text // Send reply with just the body text
await sendEmail( await sendEmail(
client, client,
[sender.email], [sender.email],
`Re: ${selectedEmail.subject || "(no subject)"}`, `Re: ${selectedEmail.subject || "(no subject)"}`,
body body,
undefined,
undefined,
primaryIdentity?.id,
primaryIdentity?.email,
undefined,
primaryIdentity?.name || undefined
); );
// Refresh emails to show the sent reply // Refresh emails to show the sent reply
+4 -1
View File
@@ -30,6 +30,7 @@ interface EmailComposerProps {
body: string; body: string;
draftId?: string; draftId?: string;
fromEmail?: string; fromEmail?: string;
fromName?: string;
identityId?: string; identityId?: string;
}) => void | Promise<void>; }) => void | Promise<void>;
onClose?: () => void; onClose?: () => void;
@@ -368,7 +369,8 @@ export function EmailComposer({
currentIdentity?.id, currentIdentity?.id,
fromEmail, fromEmail,
draftId || undefined, draftId || undefined,
uploadedAttachments uploadedAttachments,
currentIdentity?.name || undefined
); );
setDraftId(savedDraftId); setDraftId(savedDraftId);
@@ -484,6 +486,7 @@ export function EmailComposer({
body, body,
draftId: finalDraftId || undefined, draftId: finalDraftId || undefined,
fromEmail, fromEmail,
fromName: currentIdentity?.name || undefined,
identityId: currentIdentity?.id, identityId: currentIdentity?.id,
}); });
+7 -5
View File
@@ -1242,7 +1242,8 @@ export class JMAPClient {
identityId?: string, identityId?: string,
fromEmail?: string, fromEmail?: string,
draftId?: 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<string> { ): Promise<string> {
// Find the drafts mailbox // Find the drafts mailbox
const mailboxes = await this.getMailboxes(); const mailboxes = await this.getMailboxes();
@@ -1256,7 +1257,7 @@ export class JMAPClient {
// Build email object with attachments if provided // Build email object with attachments if provided
interface EmailDraft { interface EmailDraft {
from: { email: string }[]; from: { name?: string; email: string }[];
to: { email: string }[]; to: { email: string }[];
cc?: { email: string }[]; cc?: { email: string }[];
bcc?: { email: string }[]; bcc?: { email: string }[];
@@ -1268,7 +1269,7 @@ export class JMAPClient {
attachments?: { blobId: string; type: string; name: string; disposition: string }[]; attachments?: { blobId: string; type: string; name: string; disposition: string }[];
} }
const emailData: EmailDraft = { const emailData: EmailDraft = {
from: [{ email: fromEmail || this.username }], from: [{ ...(fromName ? { name: fromName } : {}), email: fromEmail || this.username }],
to: to.map(email => ({ email })), to: to.map(email => ({ email })),
cc: cc?.map(email => ({ email })), cc: cc?.map(email => ({ email })),
bcc: bcc?.map(email => ({ email })), bcc: bcc?.map(email => ({ email })),
@@ -1359,7 +1360,8 @@ export class JMAPClient {
bcc?: string[], bcc?: string[],
identityId?: string, identityId?: string,
fromEmail?: string, fromEmail?: string,
draftId?: string draftId?: string,
fromName?: string
): Promise<void> { ): Promise<void> {
const emailId = draftId || `draft-${Date.now()}`; const emailId = draftId || `draft-${Date.now()}`;
@@ -1423,7 +1425,7 @@ export class JMAPClient {
accountId: this.accountId, accountId: this.accountId,
create: { create: {
[emailId]: { [emailId]: {
from: [{ email: fromEmail || this.username }], from: [{ ...(fromName ? { name: fromName } : {}), email: fromEmail || this.username }],
to: to.map(email => ({ email })), to: to.map(email => ({ email })),
cc: cc?.map(email => ({ email })), cc: cc?.map(email => ({ email })),
bcc: bcc?.map(email => ({ email })), bcc: bcc?.map(email => ({ email })),
+6 -1
View File
@@ -46,7 +46,12 @@ export const useAuthStore = create<AuthState>()(
const client = new JMAPClient(serverUrl, username, effectivePassword); const client = new JMAPClient(serverUrl, username, effectivePassword);
await client.connect(); 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; const primaryIdentity = identities.length > 0 ? identities[0] : null;
useIdentityStore.getState().setIdentities(identities); useIdentityStore.getState().setIdentities(identities);
+3 -3
View File
@@ -53,7 +53,7 @@ interface EmailStore {
loadMoreEmails: (client: JMAPClient) => Promise<void>; loadMoreEmails: (client: JMAPClient) => Promise<void>;
fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>; fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>;
fetchQuota: (client: JMAPClient) => Promise<void>; fetchQuota: (client: JMAPClient) => Promise<void>;
sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string) => Promise<void>; sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string) => Promise<void>;
deleteEmail: (client: JMAPClient, emailId: string) => Promise<void>; deleteEmail: (client: JMAPClient, emailId: string) => Promise<void>;
markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise<void>; markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise<void>;
moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>; moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>;
@@ -314,10 +314,10 @@ export const useEmailStore = create<EmailStore>((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 }); set({ isLoading: true, error: null });
try { 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 // Refresh handled by UI layer for immediate feedback
set({ isLoading: false }); set({ isLoading: false });
} catch (error) { } catch (error) {