import { mkdir, readdir } from 'node:fs/promises'; import { join } from 'node:path'; import { simpleParser } from 'mailparser'; import type { EmailSummary, EmailMessage } from 'types'; import { rebuildIndex } from '@@/queue/handlers/gmail-sync'; import { createRouter } from '../../create-router'; import { getUserEmailDir } from '@@/data-path'; const parseHeadersOnly = async (filePath: string, id: string): Promise => { try { const file = Bun.file(filePath); const buffer = Buffer.from(await file.arrayBuffer()); const parsed = await simpleParser(buffer, { skipHtmlToText: true, skipTextToHtml: true, skipImageLinks: true }); const text = parsed.text ?? ''; const snippet = text.slice(0, 120).replace(/\s+/g, ' ').trim(); const attachmentCount = parsed.attachments?.length ?? 0; return { id, from: parsed.from?.text ?? '', to: parsed.to ? (Array.isArray(parsed.to) ? parsed.to.map((a) => a.text).join(', ') : parsed.to.text) : '', subject: parsed.subject ?? '(no subject)', date: (parsed.date ?? new Date()).toISOString(), snippet, ...(attachmentCount > 0 ? { attachmentCount } : {}), }; } catch { return null; } }; export const emailRouter = createRouter(); emailRouter.get('/messages', async (ctx) => { const email = ctx.get('user').email; const dir = getUserEmailDir(email); let filenames: string[]; try { const entries = await readdir(dir); filenames = entries.filter((f) => f.endsWith('.eml')); } catch { return ctx.json({ messages: [], total: 0 }); } const page = Number(ctx.req.query('page') ?? '1'); const limit = Number(ctx.req.query('limit') ?? '50'); const start = (page - 1) * limit; const indexFile = Bun.file(join(dir, 'index.json')); if (await indexFile.exists()) { try { const raw = await indexFile.json(); const index = Array.isArray(raw) ? null : (raw as { v?: number; entries: EmailSummary[] }); if (index?.v === 3 && index.entries.length === filenames.length) { return ctx.json({ messages: index.entries.slice(start, start + limit), total: index.entries.length }); } } catch { /* index corrupted, fall through to rebuild */ } } // Fallback: rebuild index from .eml files const summaries = rebuildIndex(dir); return ctx.json({ messages: summaries.slice(start, start + limit), total: summaries.length }); }); emailRouter.get('/messages/:id', async (ctx) => { const email = ctx.get('user').email; const id = ctx.req.param('id'); const filePath = join(getUserEmailDir(email), `${id}.eml`); const file = Bun.file(filePath); if (!(await file.exists())) { return ctx.text('Not found', 404); } try { const buffer = Buffer.from(await file.arrayBuffer()); const parsed = await simpleParser(buffer); const attachments = (parsed.attachments ?? []).map((a) => ({ filename: a.filename ?? 'unknown', size: a.size, contentType: a.contentType, })); const message: EmailMessage = { id, from: parsed.from?.text ?? '', to: parsed.to ? (Array.isArray(parsed.to) ? parsed.to.map((a) => a.text).join(', ') : parsed.to.text) : '', cc: parsed.cc ? (Array.isArray(parsed.cc) ? parsed.cc.map((a) => a.text).join(', ') : parsed.cc.text) : undefined, subject: parsed.subject ?? '(no subject)', date: (parsed.date ?? new Date()).toISOString(), snippet: (parsed.text ?? '').slice(0, 120).replace(/\s+/g, ' ').trim(), html: parsed.html || undefined, text: parsed.text || undefined, attachments, }; return ctx.json(message); } catch { return ctx.text('Failed to parse email', 500); } }); 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 filePath = join(getUserEmailDir(email), `${id}.eml`); const file = Bun.file(filePath); if (!(await file.exists())) { return ctx.text('Not found', 404); } try { const buffer = Buffer.from(await file.arrayBuffer()); const parsed = await simpleParser(buffer); const attachment = parsed.attachments[index]; if (!attachment) { return ctx.text('Attachment not found', 404); } const fileName = attachment.filename ?? 'unknown'; const attachDir = join(getUserEmailDir(email), 'attachments'); const destPath = join(attachDir, fileName); const destFile = Bun.file(destPath); if (!(await destFile.exists())) { await mkdir(attachDir, { recursive: true }); await Bun.write(destPath, attachment.content); } return ctx.json({ filePath: `Gmail/emails/attachments/${fileName}`, fileName, root: 'user-data' }); } catch { return ctx.text('Failed to extract attachment', 500); } });