import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { Plus, Trash2, Loader2, Mail, Server, KeyRound, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useClient } from 'hooks/useClient'; import { useAuth } from 'hooks/useAuth'; import { useJobs } from 'hooks/useJobs'; type EmailAccountRow = { id: number; provider: string; email: string; displayName: string | null; enabled: boolean; status: string; createdAt: string; }; type ProviderChoice = 'gmail-oauth' | 'gmail-password' | 'imap'; type FormState = { email: string; displayName: string; password: string; imapHost: string; imapPort: string; imapSecure: boolean; }; const INITIAL_FORM: FormState = { email: '', displayName: '', password: '', imapHost: '', imapPort: '993', imapSecure: true, }; export const EmailAccounts = () => { const client = useClient(); const { user } = useAuth(); const { jobs } = useJobs({ type: 'email-sync' }); const [accounts, setAccounts] = useState([]); const [loading, setLoading] = useState(true); const [showPicker, setShowPicker] = useState(false); const [activeForm, setActiveForm] = useState(null); const [form, setForm] = useState(INITIAL_FORM); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(null); const [syncing, setSyncing] = useState(null); const fetchAccounts = () => { client .get('/email/accounts') .then(setAccounts) .catch(() => {}) .finally(() => setLoading(false)); }; useEffect(() => { fetchAccounts(); }, []); // Poll accounts while any are queued/syncing to pick up status changes const hasSyncingAccount = accounts.some((a) => a.status === 'syncing' || a.status === 'queued'); useEffect(() => { if (!hasSyncingAccount) return; const interval = setInterval(fetchAccounts, 5000); return () => clearInterval(interval); }, [hasSyncingAccount]); const resetForm = () => { setActiveForm(null); setShowPicker(false); setForm(INITIAL_FORM); }; const handlePickProvider = (choice: ProviderChoice) => { setShowPicker(false); setActiveForm(choice); if (choice === 'gmail-oauth' || choice === 'gmail-password') { setForm({ ...INITIAL_FORM, imapHost: 'imap.gmail.com', imapPort: '993', imapSecure: true }); } else { setForm(INITIAL_FORM); } }; const handleGmailOAuth = async () => { setSaving(true); try { const status = await client.get<{ connected: boolean; email: string | null }>('/integrations/google/status'); if (!status.connected) { const params = new URLSearchParams({ token: client.token ?? '', origin: window.location.origin }); window.location.href = `/api/integrations/google/authorize?${params.toString()}`; return; } const result = await client.post<{ id: number; provider: string; email: string }>('/email/accounts', { provider: 'gmail', email: status.email, displayName: form.displayName || undefined, imapHost: 'imap.gmail.com', imapPort: 993, imapSecure: true, authType: 'oauth', credentials: { userIntegrationId: true }, }); toast.success(`Added ${result.email}`); resetForm(); fetchAccounts(); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to add Gmail account'); } finally { setSaving(false); } }; const handlePasswordSubmit = async () => { if (!form.email.trim() || !form.password.trim()) return; setSaving(true); try { const provider = form.imapHost === 'imap.gmail.com' ? 'gmail' : 'imap'; const result = await client.post<{ id: number; provider: string; email: string }>('/email/accounts', { provider, email: form.email.trim(), displayName: form.displayName.trim() || undefined, imapHost: form.imapHost, imapPort: Number(form.imapPort), imapSecure: form.imapSecure, authType: 'password', credentials: { password: form.password }, }); toast.success(`Added ${result.email}`); resetForm(); fetchAccounts(); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to add account'); } finally { setSaving(false); } }; const handleDelete = async (id: number) => { setDeleting(id); try { await client.delete(`/email/accounts/${id}`); setAccounts((prev) => prev.filter((a) => a.id !== id)); toast.success('Account removed'); } catch { toast.error('Failed to remove account'); } finally { setDeleting(null); } }; const handleSync = async (id: number) => { setSyncing(id); try { await client.post(`/email/accounts/${id}/sync`, {}); toast.success('Sync started'); // Update local state immediately setAccounts((prev) => prev.map((a) => (a.id === id ? { ...a, status: 'queued' } : a))); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to start sync'); } finally { setSyncing(null); } }; if (loading) return null; return (

Email Accounts

Connect email accounts for syncing your inbox. Supports Gmail and any IMAP provider.

{/* Account list */} {accounts.length > 0 && (
{accounts.map((account) => { const accountJob = jobs.find( (j) => (j.status === 'queued' || j.status === 'running') && (j.meta as Record | undefined)?.emailAccountId === account.id, ); const progress = accountJob?.steps[accountJob.currentStep]?.progress; return (

{account.displayName ?? account.email}

{account.displayName && (

{account.email}

)}
{account.status === 'queued' && ( Waiting... )} {(account.status === 'connected' || account.status === 'synced') && ( )}
{/* Sync progress */} {account.status === 'syncing' && progress && (

{progress.label ?? 'Syncing...'}

)} {account.status === 'syncing' && !progress && (

Syncing...

)}
); })}
)} {/* Provider picker */} {showPicker && !activeForm && (
)} {/* Gmail OAuth form */} {activeForm === 'gmail-oauth' && (

Add Gmail (OAuth)

setForm({ ...form, displayName: ev.target.value })} placeholder="Display name (optional)" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" />
)} {/* Gmail App Password form */} {activeForm === 'gmail-password' && (

Add Gmail (App Password)

To create an App Password:

  1. Go to{' '} Google App Passwords
  2. You may need to enable 2-Step Verification first
  3. Enter a name (e.g. "Officer") and click Create
  4. Copy the 16-character password and paste it below
setForm({ ...form, email: ev.target.value })} placeholder="your@gmail.com" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" /> setForm({ ...form, displayName: ev.target.value })} placeholder="Display name (optional)" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" /> setForm({ ...form, password: ev.target.value })} placeholder="xxxx xxxx xxxx xxxx" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" />
)} {/* Generic IMAP form */} {activeForm === 'imap' && (

Add IMAP Account

setForm({ ...form, email: ev.target.value })} placeholder="you@example.com" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" /> setForm({ ...form, displayName: ev.target.value })} placeholder="Display name (optional)" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" />
setForm({ ...form, imapHost: ev.target.value })} placeholder="imap.example.com" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" /> setForm({ ...form, imapPort: ev.target.value })} placeholder="993" className="h-9 w-20 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" />
setForm({ ...form, password: ev.target.value })} placeholder="Password" className="h-9 rounded-md border border-duck-dark/10 dark:border-foreground/10 bg-transparent px-3 text-sm" />
)} {/* Add account button */} {!showPicker && !activeForm && ( )}
); }; const StatusBadge = ({ status }: { status: string }) => { switch (status) { case 'connected': return ( Connected ); case 'queued': return ( Queued ); case 'syncing': return ( Syncing ); case 'synced': return ( Synced ); default: return null; } }; const ProviderIcon = ({ provider }: { provider: string }) => { switch (provider) { case 'gmail': return ; case 'outlook': return ; default: return ; } };