ihasmail 2.0: rebuild as Stalwart-first JMAP webmail
Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a React 19/Vite SPA. Mail (conversation view, search operators, labels, sanitised HTML, privacy image proxy, invites, undo send, templates), calendar (month/week/day/agenda, invites, free/busy, categories, context menus), contacts (JSContact, groups, vCard), files, Sieve filter builder (incl. filter-from-message with retroactive apply), vacation, identities with default + Reply-To, PWA/mobile layout, push via SSE, in-memory mock Stalwart for dev, Docker + CI.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMail } from "@/store/mail";
|
||||
import { Switch } from "@/ui/misc";
|
||||
import { toast } from "@/ui/toast";
|
||||
import { toInputDateTime, fromInputDateTime, toUTCDate } from "@/lib/dates";
|
||||
import { client, CAP } from "@/jmap/client";
|
||||
|
||||
export function VacationSettings() {
|
||||
const vacation = useMail((s) => s.vacation);
|
||||
const load = useMail((s) => s.loadVacation);
|
||||
const save = useMail((s) => s.saveVacation);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [subject, setSubject] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
const [from, setFrom] = useState("");
|
||||
const [to, setTo] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const available = client.hasCapability(CAP.vacation);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
useEffect(() => {
|
||||
if (!vacation) return;
|
||||
setEnabled(vacation.isEnabled);
|
||||
setSubject(vacation.subject ?? "");
|
||||
setBody(vacation.textBody ?? "");
|
||||
setFrom(vacation.fromDate ? toInputDateTime(new Date(vacation.fromDate)) : "");
|
||||
setTo(vacation.toDate ? toInputDateTime(new Date(vacation.toDate)) : "");
|
||||
}, [vacation]);
|
||||
|
||||
if (!available) return <div><h1>Out of office</h1><p className="lead">Vacation responses are not available for this account.</p></div>;
|
||||
|
||||
const submit = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await save({
|
||||
isEnabled: enabled,
|
||||
subject: subject || null,
|
||||
textBody: body || null,
|
||||
htmlBody: null,
|
||||
fromDate: from ? toUTCDate(fromInputDateTime(from)) : null,
|
||||
toDate: to ? toUTCDate(fromInputDateTime(to)) : null,
|
||||
});
|
||||
toast.success(enabled ? "Auto-reply is on" : "Auto-reply saved");
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Out of office</h1>
|
||||
<p className="lead">Automatically reply to people who email you while you're away. Each sender gets at most one reply.</p>
|
||||
<Switch checked={enabled} onChange={setEnabled} label="Auto-reply enabled" />
|
||||
<div className="field-row mt-16">
|
||||
<div className="field"><label>Starts (optional)</label><input className="input" type="datetime-local" value={from} onChange={(e) => setFrom(e.target.value)} /></div>
|
||||
<div className="field"><label>Ends (optional)</label><input className="input" type="datetime-local" value={to} onChange={(e) => setTo(e.target.value)} /></div>
|
||||
</div>
|
||||
<div className="field"><label>Subject</label><input className="input" value={subject} onChange={(e) => setSubject(e.target.value)} placeholder="Out of office" /></div>
|
||||
<div className="field"><label>Message</label><textarea className="textarea" rows={7} value={body} onChange={(e) => setBody(e.target.value)} placeholder="Thanks for your message. I'm away until … and will reply when I'm back." /></div>
|
||||
<button className="btn btn-primary" disabled={busy} onClick={() => void submit()}>Save</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user