import { useEffect, useRef, useState } from "react"; import { Plus, Trash2, Star, Eye, EyeOff } from "lucide-react"; import { useSettings } from "@/store/settings"; import { useMail } from "@/store/mail"; import type { Identity } from "@/jmap/types"; import { isAlwaysVisible } from "@/lib/identityVisibility"; 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, byteLength, compactHtml, signatureTooLong, SIGNATURE_LIMIT } from "@/lib/signatureHtml"; import { t } from "@/lib/i18n"; 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); const hidden = useSettings((s) => s.settings.hiddenIdentities); const updateSettings = useSettings((s) => s.update); const toggleHidden = (id: string) => updateSettings({ hiddenIdentities: hidden.includes(id) ? hidden.filter((x) => x !== id) : [...hidden, id] }); useEffect(() => { void load(); }, [load]); return (

{t("Identities & signatures")}

{t("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 && {t("Default")}}

{i.id !== defaultId && ( )} {/* Hiding is presentation only -- the identity still exists and still receives, like an unsubscribed folder. The default cannot be hidden, because it is what a new draft starts on. */} {i.mayDelete && ( )}
{hidden.includes(i.id) &&
{t("Not offered when composing. It still receives mail, and you can still send from it by showing it again.")}
} {(i.htmlSignature || i.textSignature) &&
{htmlToText(i.htmlSignature || i.textSignature).slice(0, 120)}
} {i.replyTo?.length ?
{t("Reply-To: {addresses}", { addresses: formatAddressList(i.replyTo) })}
: null}
))}

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

{hidden.length > 0 && (

{`${hidden.length} ${hidden.length === 1 ? "identity is" : "identities are"} hidden from the compose picker. Hiding every one of them would leave nothing to choose from, so in that case they are all offered again.`}

)} {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)); // The server's limit is on encoded bytes, so that is what to count and show. const sigLen = byteLength(compact); const tooLong = signatureTooLong(compact, htmlToText(compact)); 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 (signatureTooLong(clean, textSignature)) { 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(t("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={t("replies@example.com")} />{t("Replies to mail sent from this identity go here instead of the From address.")}
{t("Images are stored in your Files (folder “ihasmail”) and embedded when you send.")} {sigLen.toLocaleString()} / {SIGNATURE_LIMIT.toLocaleString()}
{tooLong &&
{t("This signature is larger than the server's {limit}-byte 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.", { limit: SIGNATURE_LIMIT })}
}
); }