import { useEffect, useRef, useState } from "react"; import { Plus, Trash2, Star } from "lucide-react"; import { useSettings } from "@/store/settings"; import { useMail } from "@/store/mail"; import type { Identity } from "@/jmap/types"; import { Dialog, confirmDialog } from "@/ui/dialog"; import { RichEditor, type RichEditorHandle } from "../compose/RichEditor"; import { toast } from "@/ui/toast"; import { parseAddressList, formatAddressList } from "@/lib/address"; import { htmlToText } from "@/lib/text"; import { sanitizeEditorHtml } from "@/lib/html"; import { externalizeDataImages, storeSignatureHtml, uploadSignatureImage } from "@/lib/signatureImages"; import { buildMarkerSignature, compactHtml, SIGNATURE_LIMIT } from "@/lib/signatureHtml"; export function IdentitiesSettings() { const identities = useMail((s) => s.identities); const load = useMail((s) => s.loadIdentities); const accountId = useMail((s) => s.accountId); const setDefault = useMail((s) => s.setDefaultIdentity); const defaultId = useSettings((s) => (accountId ? s.settings.defaultIdentityByAccount[accountId] : undefined)) ?? identities[0]?.id; const [editing, setEditing] = useState | null>(null); useEffect(() => { void load(); }, [load]); return (

Identities & signatures

Each identity is a sender address with its own name, Reply-To and signature. The default identity is preselected when you compose; set a Reply-To when replies should go somewhere other than the From address.

{identities.map((i) => (
setEditing(i)}>

{i.name ? `${i.name} <${i.email}>` : i.email} {i.id === defaultId && Default}

{i.id !== defaultId && ( )} {i.mayDelete && ( )}
{(i.htmlSignature || i.textSignature) &&
{htmlToText(i.htmlSignature || i.textSignature).slice(0, 120)}
} {i.replyTo?.length ?
Reply-To: {formatAddressList(i.replyTo)}
: null}
))}

New identities must use an address this account is allowed to send from (aliases configured on the server).

{editing && setEditing(null)} />}
); } function IdentityDialog({ identity, onClose }: { identity: Partial; onClose: () => void }) { const [name, setName] = useState(identity.name ?? ""); const [email, setEmail] = useState(identity.email ?? ""); const [replyTo, setReplyTo] = useState(formatAddressList(identity.replyTo)); const [html, setHtml] = useState(identity.htmlSignature || (identity.textSignature ? identity.textSignature.replace(/\n/g, "
") : "")); const [busy, setBusy] = useState(false); const ref = useRef(null); const compact = compactHtml(sanitizeEditorHtml(html)); const sigLen = compact.length; const tooLong = sigLen > SIGNATURE_LIMIT || htmlToText(compact).length > SIGNATURE_LIMIT; const save = async () => { setBusy(true); try { // 1) pasted pictures → stored files, 2) strip cruft, 3) fall back to a stored full copy. const externalized = await externalizeDataImages(sanitizeEditorHtml(html)); const clean = compactHtml(externalized); let htmlSignature = clean; let textSignature = htmlToText(clean); if (clean.length > SIGNATURE_LIMIT || textSignature.length > SIGNATURE_LIMIT) { const blobId = await storeSignatureHtml(clean); ({ htmlSignature, textSignature } = buildMarkerSignature(blobId, clean)); } const patch: Partial = { name, replyTo: replyTo.trim() ? parseAddressList(replyTo) : null, htmlSignature, textSignature, }; if (!identity.id) patch.email = email.trim(); await useMail.getState().saveIdentity(identity.id ?? null, patch); toast.success("Identity saved"); onClose(); } catch (err) { toast.error((err as Error).message); } finally { setBusy(false); } }; return ( }>
setName(e.target.value)} />
setEmail(e.target.value)} />
setReplyTo(e.target.value)} placeholder="replies@example.com" />Replies to mail sent from this identity go here instead of the From address.
Images are stored in your Files (folder “ihasmail”) and embedded when you send. {sigLen.toLocaleString()} / {SIGNATURE_LIMIT.toLocaleString()}
{tooLong &&
This signature is larger than the server's {SIGNATURE_LIMIT}-character limit. ihasmail will keep the full version in your Files and store a short text fallback on the server — other mail clients will see the plain-text version.
}
); }