wip: email sync via imap with status tracking and auto cron

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 04:57:30 +00:00
co-authored by Claude Opus 4.6
parent 5925ac49a1
commit 170bd6d41b
20 changed files with 1929 additions and 373 deletions
+30 -13
View File
@@ -4,9 +4,12 @@ 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');
@@ -18,7 +21,9 @@ emailRouter.get('/messages', async (ctx) => {
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 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 });
@@ -36,11 +41,11 @@ emailRouter.get('/messages/:id', async (ctx) => {
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 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 from = row.from_name ? `${row.from_name} <${row.from_address}>` : (row.from_address as string);
const message: EmailMessage = {
id: row.id as string,
@@ -75,7 +80,10 @@ emailRouter.post('/messages/:id/attachments/:index/extract', async (ctx) => {
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;
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';
@@ -144,9 +152,18 @@ emailRouter.get('/stats', async (ctx) => {
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 }>;
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 {
@@ -159,7 +176,9 @@ emailRouter.get('/labels', async (ctx) => {
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 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) {
@@ -169,9 +188,7 @@ emailRouter.get('/labels', async (ctx) => {
}
}
const labels = [...counts.entries()]
.map(([label, count]) => ({ label, count }))
.sort((a, b) => b.count - a.count);
const labels = [...counts.entries()].map(([label, count]) => ({ label, count })).sort((a, b) => b.count - a.count);
return ctx.json({ labels });
} finally {