import { useEffect, useState } from 'react' import { AlertTriangle, CheckCircle2, Inbox, PlugZap, RefreshCw, Unplug } from 'lucide-react' import { Badge, Panel } from '../components/ui' import { Labeled, Note, Toggle, btnGhost, btnPrimary, fieldCls } from '../components/form' import { CAP, can } from '../lib/can' import { useAuth } from '../lib/useAuth' import { useDisconnectMailbox, useMailAccount, useSaveMailAccount, useSetMailboxEnabled, useSyncMailbox, useTestMailbox, type MailAccountInput, } from '../lib/mailbox' // THE PERSONAL MAILBOX. // // This connects ONE recruiter's own mail account so that correspondence with candidates lands on the // candidate record without anybody copying and pasting it. It is per person, not per organization, which is // why it is its own screen and not a panel in settings: settings is owner gated, and a recruiter must be // able to connect their own mailbox without an owner doing it for them. // // WHAT IS READ IS BOUNDED AND THE SCREEN SAYS SO, prominently and before anything is connected. Headers // only, never bodies. Mail with people who are not already in this organization's records is discarded // rather than stored. Somebody handing over their work mail password deserves to read exactly that in the // place where they hand it over, not in a policy document. const empty: MailAccountInput = { imap_host: '', imap_port: 993, imap_username: '', password: '', mailbox: 'INBOX', lookback_days: 30, } export function Mailbox() { const { principal } = useAuth() const q = useMailAccount() const save = useSaveMailAccount() const test = useTestMailbox() const disconnect = useDisconnectMailbox() const setEnabled = useSetMailboxEnabled() const sync = useSyncMailbox() const [form, setForm] = useState(empty) const [confirmDisconnect, setConfirmDisconnect] = useState(false) const a = q.data?.account ?? null const connected = !!q.data?.connected && !!a // Seeded from the server once it resolves, so an edit starts from what is actually stored rather than from // blanks that would read as a mailbox that was never configured. The password is [credential removed] seeded: it is not // returned by any route, and an empty box next to "a password is [credential removed]" is the honest rendering. useEffect(() => { if (!a) return setForm({ imap_host: a.imap_host, imap_port: a.imap_port, imap_username: a.imap_username, password: '', mailbox: a.mailbox, lookback_days: a.lookback_days, }) }, [a]) if (!can(principal, CAP.candidateWrite)) { return (

Connecting a mailbox is for people who work candidate records.

Your account can read records but not change them, so there is nothing here to log correspondence against.

) } const verified = !!a?.verified_at const set = (patch: Partial) => setForm({ ...form, ...patch }) return (

Connect the mail account you already write to candidates from. missionMATCH then reads the headers of your recent mail and logs an activity on the matching candidate record, so the history on a record is what actually happened rather than what somebody remembered to type.

What is read. Only the headers: who a message was from, who it was to, the subject and the time. Never the body of a message, and never an attachment. Mail with people who are not already in this organization's records is discarded and never stored. This is your own mailbox, and nobody else in the organization can configure it or see what it read.
set({ imap_host: e.target.value })} className={fieldCls} /> set({ imap_port: Number(e.target.value) })} className={fieldCls} /> set({ imap_username: e.target.value })} className={fieldCls} /> set({ password: [credential removed] })} className={fieldCls} /> set({ mailbox: e.target.value })} className={fieldCls} />
{/* Many providers refuse an account password over IMAP and want an application specific one. Saying so here saves the support conversation that a bare authentication failure starts. */}

If your mail provider uses two step sign in, this needs an app password rather than the password you type into a browser. Your provider's security settings is where one is created.

{connected && ( )} {connected && (confirmDisconnect ? ( <> ) : ( ))}
{confirmDisconnect && ( Disconnecting removes the stored password and the sync history. The activities already logged on candidate records stay, because they are a record of what happened. )} {save.isError && {(save.error as Error).message}} {test.isError && {(test.error as Error).message}} {test.isSuccess && The mailbox answered and the sign in worked.} {a?.last_error && !test.isSuccess && !test.isError && ( The last attempt failed: {a.last_error} )}
{connected && a && (
{verified ? ( Verified ) : ( Not verified )} {a.last_sync_at ? `Last synced ${new Date(a.last_sync_at).toLocaleString()}` : 'Never synced'}
Log my candidate correspondence setEnabled.mutate(v)} />
{!verified && (
Syncing stays off until the connection is tested. An unproven mailbox never syncs, so a typo surfaces here rather than as a history that quietly stays empty.
)}
Reads the last {a.lookback_days} days of {a.mailbox}.
{sync.isError && (
{(sync.error as Error).message}
)} {sync.isSuccess && sync.data && (
{/* The discarded count is shown rather than hidden. It is the number that proves the boundary claim above: mail with people who are not in the records went nowhere. */}
)} {sync.isSuccess && sync.data?.note && (

{sync.data.note}

)} {sync.isSuccess && sync.data && sync.data.logged === 0 && (
Nothing new was logged. Either every message was already known, or none of the recent mail was with somebody in this organization's records.
)}
)}
) } function SyncStat({ label, value }: { label: string; value: number }) { return (
{value}
{label}
) }