compose: single close button + attachment support (paste, drag-drop, file picker)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 07:39:27 +00:00
co-authored by Claude Opus 4.8
parent ead8f53951
commit ced1796a56
2 changed files with 108 additions and 16 deletions
+21 -9
View File
@@ -26,20 +26,32 @@ async function getSmtpTransport(userId: number): Promise<{ transport: ReturnType
return { transport, from: acct.email };
}
// POST /send — compose/reply. Gmail auto-files the sent copy in "Sent", so IMAP sync picks it up.
// POST /send — compose/reply (multipart; `files` are attachments). Gmail auto-files the sent copy in
// "Sent", so IMAP sync picks it up.
emailRouter.post('/send', async (ctx) => {
const user = ctx.get('user');
const b = await ctx.req.json<{ to: string; cc?: string; bcc?: string; subject?: string; body?: string; inReplyTo?: string }>();
if (!b.to?.trim()) throw errors.BAD_REQUEST('At least one recipient is required');
const form = await ctx.req.parseBody({ all: true });
const str = (v: unknown) => (typeof v === 'string' ? v : '');
const to = str(form.to).trim();
if (!to) throw errors.BAD_REQUEST('At least one recipient is required');
const rawFiles = form.files;
const files = (Array.isArray(rawFiles) ? rawFiles : rawFiles ? [rawFiles] : []).filter((f): f is File => f instanceof File);
const attachments = await Promise.all(
files.map(async (f) => ({ filename: f.name, content: Buffer.from(await f.arrayBuffer()), contentType: f.type || undefined })),
);
const inReplyTo = str(form.inReplyTo).trim();
const { transport, from } = await getSmtpTransport(user.id);
await transport.sendMail({
from,
to: b.to,
cc: b.cc?.trim() || undefined,
bcc: b.bcc?.trim() || undefined,
subject: b.subject?.trim() || '(no subject)',
text: b.body ?? '',
...(b.inReplyTo ? { headers: { 'In-Reply-To': b.inReplyTo, References: b.inReplyTo } } : {}),
to,
cc: str(form.cc).trim() || undefined,
bcc: str(form.bcc).trim() || undefined,
subject: str(form.subject).trim() || '(no subject)',
text: str(form.body),
...(attachments.length ? { attachments } : {}),
...(inReplyTo ? { headers: { 'In-Reply-To': inReplyTo, References: inReplyTo } } : {}),
});
return ctx.json({ ok: true });
});