The click-to-zoom feature from the previous commit worked for cid:-referenced images but silently failed for images embedded directly as a data: URI — confirmed with a real Playwright click: Chrome refuses to navigate a tab (even a new one, even from a direct user click) to a data: URL, so <a href="data:...' target="_blank"> just does nothing. The cursor still showed zoom-in on hover since that's plain CSS, which is exactly the "лупа появляется, но не нажимается" symptom reported. Fix: imap.ts now extracts every data:image src out of an inbound email's HTML into a real attachment file (deduping identical images embedded more than once), the same way cid: images already were, and rewrites the HTML to point at that attachment's normal /api/attachments/... URL instead. That also shrinks messages.body_html (data URIs can be hundreds of KB sitting in a DB column) and gets data-URI images the same "no duplicate chip below" treatment cid: images already had. attachments.isInline is now its own real column (backfilled from the existing content_id-based cases) instead of being derived from content_id, since a data-URI-derived attachment is inline but was never cid-referenced. sanitizeEmailHtml's link-wrapping step now skips any residual data: src defensively (unwrapped-but-visible beats a link that looks clickable but isn't). Verified against the real deployment with actual browser clicks (Playwright): both a data-URI image and a cid: image now open their full-resolution attachment in a new tab; before this fix the data-URI one silently did nothing. Added a vitest.config.ts (needed for the new test file's @/ import aliases) and unit tests for the extraction/dedup logic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GteWhnWKTmnXcsd5jx6H7u
36 lines
1.5 KiB
TypeScript
36 lines
1.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { extractDataUriImages } from "./imap";
|
|
|
|
const TINY_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
|
|
|
|
describe("extractDataUriImages", () => {
|
|
it("extracts a data:image src into a real attachment and rewrites the HTML to reference it", async () => {
|
|
const html = `<p>see</p><img src="data:image/png;base64,${TINY_PNG_B64}" alt="shot">`;
|
|
const { html: out, attachments } = await extractDataUriImages(html);
|
|
|
|
expect(attachments).toHaveLength(1);
|
|
expect(attachments[0].mimeType).toBe("image/png");
|
|
expect(attachments[0].isInline).toBe(true);
|
|
expect(attachments[0].contentId).toBeNull();
|
|
expect(out).toContain(`/api/attachments/${attachments[0].id}`);
|
|
expect(out).not.toContain("base64");
|
|
});
|
|
|
|
it("dedupes the same image embedded twice into a single attachment", async () => {
|
|
const src = `data:image/png;base64,${TINY_PNG_B64}`;
|
|
const html = `<img src="${src}"><p>middle</p><img src="${src}">`;
|
|
const { html: out, attachments } = await extractDataUriImages(html);
|
|
|
|
expect(attachments).toHaveLength(1);
|
|
const occurrences = out.split(`/api/attachments/${attachments[0].id}`).length - 1;
|
|
expect(occurrences).toBe(2);
|
|
});
|
|
|
|
it("leaves HTML with no data: images untouched", async () => {
|
|
const html = '<p>hi</p><img src="https://example.com/x.png">';
|
|
const { html: out, attachments } = await extractDataUriImages(html);
|
|
expect(attachments).toHaveLength(0);
|
|
expect(out).toBe(html);
|
|
});
|
|
});
|