Initial commit: webmail client (M0/M1)

Three-pane webmail client.
- Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP
  sync, threading, search, drafts/outbox with undo, attachments, 2FA.
- Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane
  UI with rich-text composer, light/dark themes, resizable panels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 23:20:34 +02:00
co-authored by Claude Opus 4.8
commit a5ec0d21cb
174 changed files with 22239 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useT } from "../i18n";
import { api, ApiError } from "../lib/api";
export function Login() {
const t = useT();
const [handle, setHandle] = useState("");
const [password, setPassword] = useState("");
const [totp, setTotp] = useState("");
const [needTotp, setNeedTotp] = useState(false);
const navigate = useNavigate();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: () => api.login(handle, password, needTotp ? totp : undefined),
onSuccess: (user) => {
queryClient.setQueryData(["me"], user);
navigate("/mail");
},
onError: (error) => {
if (error instanceof ApiError && (error.code === "totp_required" || error.code === "totp_invalid")) {
setNeedTotp(true);
}
},
});
const error = mutation.error instanceof ApiError ? mutation.error : null;
// Don't show the "code required" error as a failure once we've revealed the field.
const showError = error && !(needTotp && error.code === "totp_required");
return (
<div className="mx-auto mt-24 max-w-sm px-4">
<h1 className="mb-6 text-2xl font-semibold">{t("auth.signInTitle")}</h1>
<form
className="space-y-3"
onSubmit={(e) => {
e.preventDefault();
mutation.mutate();
}}
>
<input
className="w-full rounded-lg border border-app-border bg-app-surface px-3 py-2 text-app-fg placeholder:text-app-faint focus:border-app-accent focus:outline-none"
placeholder={t("auth.handlePlaceholder")}
value={handle}
onChange={(e) => setHandle(e.target.value)}
disabled={needTotp}
autoFocus
/>
<input
className="w-full rounded-lg border border-app-border bg-app-surface px-3 py-2 text-app-fg placeholder:text-app-faint focus:border-app-accent focus:outline-none"
type="password"
placeholder={t("auth.password")}
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={needTotp}
/>
{needTotp && (
<div className="space-y-1">
<input
className="w-full rounded-lg border border-app-border bg-app-surface px-3 py-2 text-app-fg placeholder:text-app-faint focus:border-app-accent focus:outline-none tracking-widest"
placeholder="123456"
inputMode="numeric"
autoComplete="one-time-code"
value={totp}
onChange={(e) => setTotp(e.target.value)}
autoFocus
/>
<p className="text-xs text-app-muted">{t("auth.totpPrompt")}</p>
</div>
)}
<button
className="w-full rounded-lg bg-app-accent py-2 font-medium text-app-accent-fg hover:opacity-90 disabled:opacity-50"
disabled={mutation.isPending}
type="submit"
>
{mutation.isPending ? t("auth.signingIn") : t("auth.signIn")}
</button>
{showError && <p className="text-sm text-red-500">{error.message}</p>}
</form>
<p className="mt-4 text-sm text-app-muted">
{t("auth.noAccount")}{" "}
<Link className="text-app-accent" to="/register">
{t("auth.register")}
</Link>
</p>
</div>
);
}