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] 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
+13 -2
View File
@@ -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<NodeJS.Timeout | null>(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
+4 -1
View File
@@ -30,6 +30,7 @@ interface EmailComposerProps {
body: string;
draftId?: string;
fromEmail?: string;
fromName?: string;
identityId?: string;
}) => void | Promise<void>;
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,
});
+7 -5
View File
@@ -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<string> {
// 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<void> {
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 })),
+6 -1
View File
@@ -46,7 +46,12 @@ export const useAuthStore = create<AuthState>()(
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);
+3 -3
View File
@@ -53,7 +53,7 @@ interface EmailStore {
loadMoreEmails: (client: JMAPClient) => Promise<void>;
fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>;
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>;
markAsRead: (client: JMAPClient, emailId: string, read: boolean) => 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 });
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) {