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