142 lines
4.8 KiB
TypeScript
142 lines
4.8 KiB
TypeScript
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<EmailSummary | null> => {
|
|
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);
|
|
}
|
|
});
|