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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 })),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user