diff --git a/src/io/outlookMail.test.ts b/src/io/outlookMail.test.ts
index c708f7e..5e2aeff 100644
--- a/src/io/outlookMail.test.ts
+++ b/src/io/outlookMail.test.ts
@@ -254,6 +254,82 @@ describe("outlookMail helpers", () => {
]);
});
+ it("embeds inline CID images in imported EML HTML body", async () => {
+ const imageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
+ const eml = [
+ "From: sender@example.com",
+ "To: reader@example.com",
+ "Subject: Inline logo",
+ "Date: Mon, 01 Jun 2026 10:00:00 +0300",
+ 'Content-Type: multipart/related; boundary="related"',
+ "",
+ "--related",
+ "Content-Type: text/html; charset=utf-8",
+ "",
+ '
Logo
',
+ "--related",
+ 'Content-Type: image/png; name="logo.png"',
+ 'Content-Disposition: inline; filename="logo.png"',
+ "Content-ID: ",
+ "Content-Transfer-Encoding: base64",
+ "",
+ imageBase64,
+ "--related--"
+ ].join("\r\n");
+
+ const imported = await importEmlFile(new File([eml], "inline.eml", { type: "message/rfc822" }));
+
+ expect(imported.body).toBe("Logo");
+ expect(imported.bodyHtml).toContain(`src="data:image/png;base64,${imageBase64}"`);
+ expect(imported.bodyHtml).not.toContain("cid:");
+ expect(imported.attachments).toEqual([
+ {
+ id: "attachment-logo-png-1",
+ fileName: "logo.png",
+ contentType: "image/png",
+ size: 68,
+ contentBase64: imageBase64,
+ disposition: "inline",
+ contentId: "logo@example.com"
+ }
+ ]);
+ });
+
+ it("keeps non-image CID references in imported EML HTML body", async () => {
+ const eml = [
+ "From: sender@example.com",
+ "To: reader@example.com",
+ "Subject: Inline document",
+ "Date: Mon, 01 Jun 2026 10:00:00 +0300",
+ 'Content-Type: multipart/related; boundary="related"',
+ "",
+ "--related",
+ "Content-Type: text/html; charset=utf-8",
+ "",
+ 'Document
',
+ "--related",
+ 'Content-Type: text/plain; name="document.txt"',
+ 'Content-Disposition: inline; filename="document.txt"',
+ "Content-ID: ",
+ "Content-Transfer-Encoding: base64",
+ "",
+ "SGVsbG8=",
+ "--related--"
+ ].join("\r\n");
+
+ const imported = await importEmlFile(new File([eml], "inline.eml", { type: "message/rfc822" }));
+
+ expect(imported.bodyHtml).toContain('src="cid:document"');
+ expect(imported.bodyHtml).not.toContain("data:text/plain");
+ expect(imported.attachments?.[0]).toMatchObject({
+ fileName: "document.txt",
+ contentType: "text/plain",
+ contentBase64: "SGVsbG8=",
+ disposition: "inline",
+ contentId: "document"
+ });
+ });
+
it("преобразует данные Outlook MSG в письмо QOutlook с вложениями", async () => {
const message = msgDataToMailMessage(
{
diff --git a/src/io/outlookMail.ts b/src/io/outlookMail.ts
index 609b869..d94dd3f 100644
--- a/src/io/outlookMail.ts
+++ b/src/io/outlookMail.ts
@@ -81,7 +81,8 @@ export async function importEmlFile(file: File): Promise {
const raw = await file.text();
const parsed = parseEmlSource(raw);
- const bodyHtml = extractMessageHtmlBody(parsed.headers, parsed.body);
+ const attachments = extractMessageAttachments(parsed.headers, parsed.body);
+ const bodyHtml = htmlWithEmbeddedCidAttachments(extractMessageHtmlBody(parsed.headers, parsed.body), attachments);
return {
id: `eml-${Date.now()}`,
@@ -99,7 +100,7 @@ export async function importEmlFile(file: File): Promise {
time: parsed.headers.date || new Date().toLocaleString("ru-RU"),
read: false,
flagged: false,
- attachments: extractMessageAttachments(parsed.headers, parsed.body)
+ attachments
};
}
@@ -262,6 +263,41 @@ export function extractMessageAttachments(headers: MailHeaders, body: string): M
return collectAttachmentParts(headers, body).map((part, index) => attachmentFromPart(part, index));
}
+export function htmlWithEmbeddedCidAttachments(html: string, attachments: MailAttachment[] = []): string {
+ if (!html.trim() || attachments.length === 0) {
+ return html;
+ }
+
+ const inlineImagesByContentId = new Map();
+ attachments.forEach((attachment) => {
+ const contentId = normalizedContentId(attachment.contentId);
+ const contentType = sanitizeMimeType(attachment.contentType);
+ const contentBase64 = attachment.contentBase64.trim();
+ if (!contentId || attachment.disposition !== "inline" || !contentBase64 || !isEmbeddableCidImageContentType(contentType)) {
+ return;
+ }
+
+ if (!inlineImagesByContentId.has(contentId)) {
+ inlineImagesByContentId.set(contentId, { contentBase64, contentType });
+ }
+ });
+
+ if (inlineImagesByContentId.size === 0) {
+ return html;
+ }
+
+ return html.replace(/\bsrc\s*=\s*(?:(["'])\s*cid:([^"'>\s]+)\1|cid:([^\s"'<>]+))/gi, (match, quote: string | undefined, quotedCid: string | undefined, unquotedCid: string | undefined) => {
+ const contentId = normalizedContentId(decodeUriComponentSafe(quotedCid ?? unquotedCid ?? ""));
+ const attachment = inlineImagesByContentId.get(contentId);
+ if (!attachment) {
+ return match;
+ }
+
+ const dataUri = `data:${attachment.contentType};base64,${attachment.contentBase64}`;
+ return quote ? `src=${quote}${dataUri}${quote}` : `src="${dataUri}"`;
+ });
+}
+
export async function createMailAttachmentFromFile(file: File): Promise {
return {
id: createAttachmentId(file.name),
@@ -783,6 +819,22 @@ function sanitizeMimeType(value = "application/octet-stream"): string {
return /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/i.test(mediaType) ? mediaType : "application/octet-stream";
}
+function isEmbeddableCidImageContentType(value: string): boolean {
+ return ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/bmp", "image/webp"].includes(value.toLowerCase());
+}
+
+function normalizedContentId(value = ""): string {
+ return value.trim().replace(/^<|>$/g, "").toLowerCase();
+}
+
+function decodeUriComponentSafe(value: string): string {
+ try {
+ return decodeURIComponent(value);
+ } catch {
+ return value;
+ }
+}
+
function wrapBase64(value: string): string {
return (value.match(/.{1,76}/g) ?? [""]).join("\r\n");
}