import { uuidv7 } from '@sims/domain' import { writeAudit } from './audit' import type { DB } from './db' /** * Email account + send log — plain functions over the handle (D12 pattern). * One sending identity: saveAccount replaces any existing row. The refresh * token is stored AES-256-GCM encrypted (crypto.ts) and never appears in * audit rows or logs. */ export interface EmailAccount { id: string; address: string; refreshTokenEnc: string status: 'active' | 'dead'; updatedAt: string } interface AccountRow { id: string; address: string; refresh_token_enc: string; status: string; updated_at: string } function toAccount(r: AccountRow): EmailAccount { return { id: r.id, address: r.address, refreshTokenEnc: r.refresh_token_enc, status: r.status as EmailAccount['status'], updatedAt: r.updated_at, } } export async function getAccount(db: DB): Promise { const row = await db.get(`SELECT * FROM email_account ORDER BY id DESC LIMIT 1`) return row === undefined ? null : toAccount(row) } export async function saveAccount(db: DB, address: string, refreshTokenEnc: string): Promise { return db.transaction(async () => { await db.run(`DELETE FROM email_account`) // single sending identity const id = uuidv7() await db.run( `INSERT INTO email_account (id, address, refresh_token_enc, status, updated_at) VALUES (?, ?, ?, 'active', ?)`, id, address, refreshTokenEnc, new Date().toISOString(), ) // Audit carries the address only — never the (even encrypted) token. await writeAudit(db, 'system', 'create', 'email_account', id, undefined, { address, status: 'active' }) return (await getAccount(db))! }) } /** Refresh token revoked/expired (invalid_grant) — flips the dashboard banner. */ export async function markAccountDead(db: DB): Promise { const account = await getAccount(db) if (account === null || account.status === 'dead') return await db.run(`UPDATE email_account SET status='dead', updated_at=? WHERE id=?`, new Date().toISOString(), account.id) await writeAudit(db, 'system', 'update', 'email_account', account.id, { status: account.status }, { status: 'dead' }) } export interface EmailLogInput { documentId?: string; to: string; subject: string status: 'sent' | 'failed'; gmailMessageId?: string; error?: string } /** Append-only send trace — the email_log table is itself the record. */ export async function logEmail(db: DB, e: EmailLogInput): Promise { await db.run( `INSERT INTO email_log (id, document_id, to_addr, subject, status, gmail_message_id, error, at_wall) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, uuidv7(), e.documentId ?? null, e.to, e.subject, e.status, e.gmailMessageId ?? null, e.error ?? null, new Date().toISOString(), ) } export interface EmailStatus { connected: boolean; address?: string; dead: boolean } export async function emailStatus(db: DB): Promise { const account = await getAccount(db) if (account === null) return { connected: false, dead: false } return { connected: true, address: account.address, dead: account.status === 'dead' } }