import { mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import type { EmailMessage } from 'types'; import { createRouter } from '../../create-router'; import { DATA_PATH } from '@@/data-path'; import { openEmailDb, rowToSummary, getSyncMeta } from './email-db'; import { accountsRouter } from './accounts'; export const emailRouter = createRouter(); emailRouter.route('/accounts', accountsRouter); emailRouter.get('/messages', async (ctx) => { const email = ctx.get('user').email; const page = Number(ctx.req.query('page') ?? '1'); const limit = Number(ctx.req.query('limit') ?? '50'); const folder = ctx.req.query('folder') ?? 'inbox'; const offset = (page - 1) * limit; const folderWhere = folder === 'all' ? 'deleted = 0' : `deleted = 0 AND labels LIKE '%${folder}%'`; const db = openEmailDb(email); try { const rows = db .query(`SELECT * FROM emails WHERE ${folderWhere} ORDER BY date DESC LIMIT ? OFFSET ?`) .all(limit, offset) as Record[]; const countRow = db.query(`SELECT COUNT(*) as total FROM emails WHERE ${folderWhere}`).get() as { total: number }; const messages = rows.map(rowToSummary); return ctx.json({ messages, total: countRow.total }); } finally { db.close(); } }); emailRouter.get('/messages/:id', async (ctx) => { const email = ctx.get('user').email; const id = ctx.req.param('id'); const db = openEmailDb(email); try { const row = db.query('SELECT * FROM emails WHERE id = ? AND deleted = 0').get(id) as Record | null; if (!row) return ctx.text('Not found', 404); const attachmentRows = db.query('SELECT * FROM attachments WHERE email_id = ? ORDER BY idx').all(id) as Array< Record >; const from = row.from_name ? `${row.from_name} <${row.from_address}>` : (row.from_address as string); const message: EmailMessage = { id: row.id as string, from, to: row.to_address as string, cc: (row.cc as string) ?? undefined, subject: row.subject as string, date: row.date as string, snippet: row.snippet as string, html: (row.html as string) ?? undefined, text: (row.text_body as string) ?? undefined, attachments: attachmentRows.map((a) => ({ filename: a.filename as string, size: a.size as number, contentType: a.content_type as string, })), ...(row.attachment_count ? { attachmentCount: row.attachment_count as number } : {}), ...(row.read ? { read: true } : {}), ...(row.labels ? { labels: (row.labels as string).split(',') } : {}), }; return ctx.json(message); } finally { db.close(); } }); emailRouter.post('/messages/:id/attachments/:index/extract', async (ctx) => { const email = ctx.get('user').email; const id = ctx.req.param('id'); const index = Number(ctx.req.param('index')); const db = openEmailDb(email); try { const row = db.query('SELECT filename, content FROM attachments WHERE email_id = ? AND idx = ?').get(id, index) as { filename: string; content: string | null; } | null; if (!row || !row.content) return ctx.text('Attachment not found', 404); const fileName = row.filename ?? 'unknown'; const attachDir = join(DATA_PATH, email, 'Gmail', 'emails', 'attachments'); const destPath = join(attachDir, fileName); const destFile = Bun.file(destPath); if (!(await destFile.exists())) { await mkdir(attachDir, { recursive: true }); const binary = Buffer.from(row.content, 'base64'); await Bun.write(destPath, binary); } return ctx.json({ filePath: `Gmail/emails/attachments/${fileName}`, fileName, root: 'user-data' }); } catch { return ctx.text('Failed to extract attachment', 500); } finally { db.close(); } }); emailRouter.patch('/messages/:id/read', async (ctx) => { const email = ctx.get('user').email; const id = ctx.req.param('id'); const db = openEmailDb(email); try { db.run('UPDATE emails SET read = 1 WHERE id = ? AND read = 0', [id]); return ctx.json({ ok: true }); } finally { db.close(); } }); emailRouter.delete('/messages/:id', async (ctx) => { const email = ctx.get('user').email; const id = ctx.req.param('id'); const db = openEmailDb(email); try { const result = db.run('UPDATE emails SET deleted = 1 WHERE id = ? AND deleted = 0', [id]); if (result.changes === 0) return ctx.text('Not found', 404); return ctx.json({ ok: true }); } finally { db.close(); } }); emailRouter.get('/sync-status', async (ctx) => { const email = ctx.get('user').email; const db = openEmailDb(email); try { const lastSyncAt = getSyncMeta(db, 'last_sync_at'); return ctx.json({ lastSyncAt }); } finally { db.close(); } }); emailRouter.get('/stats', async (ctx) => { const email = ctx.get('user').email; const folder = ctx.req.query('folder') ?? 'inbox'; const folderWhere = folder === 'all' ? 'deleted = 0' : `deleted = 0 AND labels LIKE '%${folder}%'`; const db = openEmailDb(email); try { const total = (db.query(`SELECT COUNT(*) as count FROM emails WHERE ${folderWhere}`).get() as { count: number }) .count; const byDomain = db .query( `SELECT from_domain, COUNT(*) as count FROM emails WHERE ${folderWhere} GROUP BY from_domain ORDER BY count DESC LIMIT 20`, ) .all() as Array<{ from_domain: string; count: number }>; const bySender = db .query( `SELECT from_address, from_name, COUNT(*) as count FROM emails WHERE ${folderWhere} GROUP BY from_address ORDER BY count DESC LIMIT 20`, ) .all() as Array<{ from_address: string; from_name: string; count: number }>; return ctx.json({ total, byDomain, bySender }); } finally { db.close(); } }); emailRouter.get('/labels', async (ctx) => { const email = ctx.get('user').email; const db = openEmailDb(email); try { const rows = db.query('SELECT labels FROM emails WHERE deleted = 0 AND labels IS NOT NULL').all() as Array<{ labels: string; }>; const counts = new Map(); for (const row of rows) { for (const label of row.labels.split(',')) { const trimmed = label.trim().toLowerCase(); if (trimmed) counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1); } } const labels = [...counts.entries()].map(([label, count]) => ({ label, count })).sort((a, b) => b.count - a.count); return ctx.json({ labels }); } finally { db.close(); } });