import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { QRCodeSVG } from "qrcode.react"; import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { RichTextEditor } from "../../components/compose/RichTextEditor"; import { useT } from "../../i18n"; import { api, ApiError } from "../../lib/api"; import { useMe } from "../../lib/auth"; import { useTheme } from "../../lib/theme"; import { PluginSlot } from "../../plugins/PluginSlot"; const SECTIONS = ["profile", "security", "accounts", "appearance"] as const; type Section = (typeof SECTIONS)[number]; export function Settings() { const t = useT(); const params = useParams(); const section = (SECTIONS.includes(params.section as Section) ? params.section : "profile") as Section; return (
← {t("nav.mail")} {t("settings.title")}
{section === "profile" && } {section === "security" && } {section === "accounts" && } {section === "appearance" && } {/* Plugin-contributed settings UI (SPEC §11.3 settings.page), under Accounts. Matches the accounts section width (max-w-5xl) with a gap above it. */} {section === "accounts" && (
)}
); } function ProfileSection() { const t = useT(); const { data } = useMe(); return (

{t("settings.profile")}

); } function Row({ label, value }: { label: string; value: string }) { return (
{label} {value}
); } function SecuritySection() { const t = useT(); const queryClient = useQueryClient(); const { data: me } = useMe(); const [setup, setSetup] = useState<{ secret: string; uri: string } | null>(null); const [code, setCode] = useState(""); const [disabling, setDisabling] = useState(false); const [disablePw, setDisablePw] = useState(""); const [error, setError] = useState(""); const sessions = useQuery({ queryKey: ["sessions"], queryFn: api.sessions }); const enabled = Boolean(me?.totp_enabled); const startSetup = async () => { const res = await api.totpSetup(); setSetup({ secret: res.secret, uri: res.provisioning_uri }); }; const confirm = async () => { setError(""); try { await api.totpEnable(code); setSetup(null); queryClient.invalidateQueries({ queryKey: ["me"] }); } catch (e) { setError(e instanceof ApiError ? e.message : String(e)); } }; const disable = async () => { setError(""); try { await api.totpDisable(disablePw); setDisabling(false); setDisablePw(""); queryClient.invalidateQueries({ queryKey: ["me"] }); } catch (e) { setError(e instanceof ApiError ? e.message : String(e)); } }; const revoke = useMutation({ mutationFn: (sid: string) => api.revokeSession(sid), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["sessions"] }), }); return (

{t("settings.twoFactor")}

{enabled ? (

{t("settings.twoFactorOn")}

{disabling ? (

{t("settings.disable2faPrompt")}

setDisablePw(e.target.value)} autoFocus />
) : ( )}
) : setup ? (

{t("settings.scanQr")}

{t("settings.secret")}:
{setup.secret}

otpauth URI
{setup.uri}
setCode(e.target.value)} />
) : ( )} {error &&

{error}

}

{t("settings.sessions")}

); } function AccountsSection() { const t = useT(); const navigate = useNavigate(); const queryClient = useQueryClient(); const accounts = useQuery({ queryKey: ["accounts"], queryFn: api.accounts }); const [editing, setEditing] = useState<{ id: string; mode: "account" | "signature" } | null>( null, ); const remove = useMutation({ mutationFn: (id: string) => api.deleteAccount(id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["accounts"] }), }); const downloadAll = useMutation({ mutationFn: (id: string) => api.updateAccount(id, { sync_window: "all" }), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["accounts"] }), }); const close = () => { setEditing(null); queryClient.invalidateQueries({ queryKey: ["accounts"] }); }; return (

{t("settings.accounts")}

); } function AccountEditor({ accountId, onCancel, onSaved, }: { accountId: string; onCancel: () => void; onSaved: () => void; }) { const t = useT(); const detail = useQuery({ queryKey: ["account", accountId], queryFn: () => api.accountDetail(accountId) }); const [displayName, setDisplayName] = useState(""); const [color, setColor] = useState("#888888"); const [showServer, setShowServer] = useState(false); const [imapHost, setImapHost] = useState(""); const [imapPort, setImapPort] = useState("993"); const [imapTls, setImapTls] = useState("ssl"); const [smtpHost, setSmtpHost] = useState(""); const [smtpPort, setSmtpPort] = useState("465"); const [smtpTls, setSmtpTls] = useState("ssl"); const [password, setPassword] = useState(""); const [error, setError] = useState(""); useEffect(() => { const d = detail.data; if (!d) return; setDisplayName(d.display_name); setColor(d.color); setImapHost(String(d.imap_config.host ?? "")); setImapPort(String(d.imap_config.port ?? "993")); setImapTls(String(d.imap_config.tls ?? "ssl")); setSmtpHost(String(d.smtp_config.host ?? "")); setSmtpPort(String(d.smtp_config.port ?? "465")); setSmtpTls(String(d.smtp_config.tls ?? "ssl")); }, [detail.data]); const save = useMutation({ mutationFn: () => { const patch: Parameters[1] = { display_name: displayName, color, }; if (showServer) { patch.imap = { host: imapHost, port: Number(imapPort), tls: imapTls, auth: "password" }; patch.smtp = { host: smtpHost, port: Number(smtpPort), tls: smtpTls, auth: "password" }; patch.password = password; } return api.updateAccount(accountId, patch); }, onSuccess: onSaved, onError: (e) => setError(e instanceof ApiError ? e.message : "Save failed"), }); if (!detail.data) return
{t("common.loading")}
; const canSave = !save.isPending && (!showServer || password.length > 0); return (
setColor(e.target.value)} aria-label={t("settings.color")} /> setDisplayName(e.target.value)} />
{!showServer ? ( ) : (

{t("settings.serverSettingsHint")}

setPassword(e.target.value)} />
)} {error &&

{error}

}
); } function SignatureEditor({ accountId, onCancel, onSaved, }: { accountId: string; onCancel: () => void; onSaved: () => void; }) { const t = useT(); const detail = useQuery({ queryKey: ["account", accountId], queryFn: () => api.accountDetail(accountId), }); const [signature, setSignature] = useState(""); const [error, setError] = useState(""); useEffect(() => { if (detail.data) setSignature(detail.data.signature); }, [detail.data]); const save = useMutation({ mutationFn: () => api.updateAccount(accountId, { signature }), onSuccess: onSaved, onError: (e) => setError(e instanceof ApiError ? e.message : "Save failed"), }); if (!detail.data) return
{t("common.loading")}
; return (
{t("settings.signature")} — {detail.data.email}
{error &&

{error}

}
); } function ServerRow({ label, host, port, tls, onHost, onPort, onTls, }: { label: string; host: string; port: string; tls: string; onHost: (v: string) => void; onPort: (v: string) => void; onTls: (v: string) => void; }) { const t = useT(); return (
{label} onHost(e.target.value)} /> onPort(e.target.value)} />
); } function AppearanceSection() { const t = useT(); const queryClient = useQueryClient(); const { theme, setTheme } = useTheme(); const settings = useQuery({ queryKey: ["settings"], queryFn: api.settings }); const patch = useMutation({ mutationFn: api.patchSettings, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["settings"] }), }); return (

{t("settings.appearance")}

{t("settings.theme")}
{(["light", "dark", "system"] as const).map((value) => ( ))}
); }