Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 357x 357x 13x 13x 13x 13x 12x 13x 13x 13x 13x 13x 13x 357x 4x 4x 4x 4x 4x 3x 4x 4x 357x | import type Database from "better-sqlite3-multiple-ciphers";
import { Hono } from "hono";
import { decompressBuffer } from "../../storage/compression.js";
import { parseIntParam } from "../validation.js";
export function attachmentRoutes(getDb: () => Database.Database): Hono {
const api = new Hono();
api.get("/:attachmentId", (c) => {
const attachmentId = parseIntParam(c, "attachmentId", c.req.param("attachmentId"));
Iif (attachmentId instanceof Response) return attachmentId;
const attachment = getDb()
.prepare(
`SELECT a.filename, a.content_type, b.data
FROM attachments a
JOIN blobs.attachment_blobs b ON b.content_hash = a.content_hash
WHERE a.id = ?`,
)
.get(attachmentId) as
| { filename: string | null; content_type: string | null; data: Buffer | null }
| undefined;
if (!attachment) return c.json({ error: "Attachment not found" }, 404);
const contentType = attachment.content_type ?? "application/octet-stream";
const rawName = attachment.filename ?? "attachment";
const safeName = rawName
.replace(/[/\\]/g, "_")
.replace(/["\r\n]/g, "")
.replace(/[^\x20-\x7E]/g, "_");
// RFC 5987: if the original name contains non-ASCII characters, include
// filename*=UTF-8''<pct-encoded> so modern browsers use the real name.
const hasNonAscii = /[^\x20-\x7E]/.test(rawName);
const contentDisposition = hasNonAscii
? `attachment; filename="${safeName}"; filename*=UTF-8''${encodeURIComponent(rawName)}`
: `attachment; filename="${safeName}"`;
const data = decompressBuffer(attachment.data);
return new Response(data ? new Uint8Array(data) : null, {
headers: {
"Content-Type": contentType,
"Content-Disposition": contentDisposition,
},
});
});
// Serve inline images by Content-ID (for cid: URL resolution in HTML emails)
api.get("/by-cid/:messageId/:contentId", (c) => {
const messageId = parseIntParam(c, "messageId", c.req.param("messageId"));
Iif (messageId instanceof Response) return messageId;
const contentId = c.req.param("contentId");
const attachment = getDb()
.prepare(
`SELECT a.content_type, b.data
FROM attachments a
JOIN blobs.attachment_blobs b ON b.content_hash = a.content_hash
WHERE a.message_id = ? AND a.content_id = ?`,
)
.get(messageId, contentId) as
| { content_type: string | null; data: Buffer | null }
| undefined;
if (!attachment) return c.json({ error: "Attachment not found" }, 404);
const contentType = attachment.content_type ?? "application/octet-stream";
const data = decompressBuffer(attachment.data);
return new Response(data ? new Uint8Array(data) : null, {
headers: {
"Content-Type": contentType,
"Cache-Control": "private, max-age=86400",
},
});
});
return api;
}
|