import { createFileRoute } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Loader2, Inbox, KeyRound, RefreshCcw } from "lucide-react"; import { toast } from "sonner"; export const Route = createFileRoute("/settings/imap")({ component: ImapSettingsPage, }); const EMPTY = { host: "", port: "993", secure: true, username: "", folder: "INBOX", enabled: true, notes: "", }; function ImapSettingsPage() { const { user, isAdmin } = useAuth(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [polling, setPolling] = useState(false); const [recordId, setRecordId] = useState(null); const [form, setForm] = useState({ ...EMPTY }); const [meta, setMeta] = useState<{ last_polled_at: string | null; last_uid: number | null; last_error: string | null; }>({ last_polled_at: null, last_uid: null, last_error: null }); const load = async () => { setLoading(true); const { data } = await supabase .from("imap_settings") .select("*") .order("updated_at", { ascending: false }) .limit(1) .maybeSingle(); if (data) { setRecordId(data.id); setForm({ host: data.host ?? "", port: String(data.port ?? 993), secure: !!data.secure, username: data.username ?? "", folder: data.folder ?? "INBOX", enabled: data.enabled ?? true, notes: data.notes ?? "", }); setMeta({ last_polled_at: data.last_polled_at, last_uid: data.last_uid, last_error: data.last_error, }); } setLoading(false); }; useEffect(() => { if (isAdmin) load(); else setLoading(false); }, [isAdmin]); const update = (k: keyof typeof EMPTY, v: string | boolean) => setForm((f) => ({ ...f, [k]: v })); const save = async () => { if (!form.host || !form.username) { toast.error("Host and username are required"); return; } setSaving(true); const payload = { host: form.host.trim(), port: parseInt(form.port, 10) || 993, secure: form.secure, username: form.username.trim(), folder: form.folder.trim() || "INBOX", enabled: form.enabled, notes: form.notes || null, updated_by: user?.id, }; const { error } = recordId ? await supabase.from("imap_settings").update(payload).eq("id", recordId) : await supabase.from("imap_settings").insert(payload); setSaving(false); if (error) { toast.error("Could not save", { description: error.message }); return; } toast.success("IMAP settings saved"); load(); }; const pollNow = async () => { setPolling(true); try { const res = await fetch("/hooks/poll-imap", { method: "POST" }); const body = await res.json(); if (!res.ok || body.error) { toast.error("Poll failed", { description: body.error }); } else { toast.success(`Imported ${body.imported ?? 0} new email(s)`); } } catch (e) { toast.error("Poll failed", { description: e instanceof Error ? e.message : String(e), }); } finally { setPolling(false); load(); } }; if (!isAdmin) { return (
Only administrators can manage IMAP settings.
); } if (loading) { return (
); } return (
Incoming mail (IMAP) update("host", e.target.value)} placeholder="imap.gmail.com" /> update("port", e.target.value)} placeholder="993" /> update("username", e.target.value)} placeholder="inbox@yourfirm.com" /> update("folder", e.target.value)} placeholder="INBOX" />
Stored securely as the IMAP_PASSWORD secret. For Gmail/Workspace, use an App Password.
Use TLS / SSL
Required for port 993. Off for 143 (STARTTLS).
update("secure", v)} />