198 lines
6.4 KiB
TypeScript
198 lines
6.4 KiB
TypeScript
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<string, unknown>[];
|
|
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<string, unknown> | 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<string, unknown>
|
|
>;
|
|
|
|
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<string, number>();
|
|
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();
|
|
}
|
|
});
|