import { describe, expect, it } from "vitest"; import { attachmentToBlob, buildEmlSource, createMailAttachmentFromFile, decodeMimeWords, decodeTransferBody, extractMessageAttachments, extractMessageBody, extractMessageHtmlBody, htmlToPlainText, importEmlFile, isSupportedEmlFileName, isSupportedMailFileName, isSupportedMsgFileName, msgDataToMailMessage, parseContentType, parseEmlHeaders, parseEmlSource, parseMultipartBody } from "./outlookMail"; import type { MailMessage } from "../types"; describe("outlookMail helpers", () => { it("разбирает folded EML headers", () => { expect(parseEmlHeaders("Subject: Первая строка\r\n\tвторая строка\r\nFrom: user@example.com")).toEqual({ subject: "Первая строка вторая строка", from: "user@example.com" }); }); it("разделяет заголовки и тело EML", () => { const parsed = parseEmlSource("From: a@example.com\r\nTo: b@example.com\r\n\r\nТекст письма"); expect(parsed.headers.from).toBe("a@example.com"); expect(parsed.body).toBe("Текст письма"); }); it("декодирует UTF-8 MIME words и transfer encodings", () => { expect(decodeMimeWords("=?UTF-8?B?0J/RgNC40LLQtdGC?=")).toBe("Привет"); expect(decodeMimeWords("=?windows-1251?B?z/Do4uXy?=")).toBe("Привет"); expect(decodeMimeWords("=?windows-1251?Q?=CF=F0=E8=E2=E5=F2?=")).toBe("Привет"); expect(decodeMimeWords("=?UTF-8?B?0J/RgNC4?= =?UTF-8?B?0LLQtdGC?=")).toBe("Привет"); expect(decodeTransferBody("0J/RgNC40LLQtdGC", "base64")).toBe("Привет"); expect(decodeTransferBody("=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82", "quoted-printable")).toBe("Привет"); }); it("разбирает Content-Type с quoted boundary и charset", () => { expect(parseContentType('multipart/alternative; boundary="----=_Next;Part"; charset=utf-8')).toEqual({ mediaType: "multipart/alternative", parameters: { boundary: "----=_Next;Part", charset: "utf-8" } }); }); it("разбирает multipart body на MIME-части", () => { const parts = parseMultipartBody( [ "Преамбула", "--qoffice-boundary", "Content-Type: text/plain; charset=utf-8", "", "Текстовая версия", "--qoffice-boundary", "Content-Type: text/html; charset=utf-8", "", "

HTML версия

", "--qoffice-boundary--" ].join("\r\n"), "qoffice-boundary" ); expect(parts).toEqual([ { headers: { "content-type": "text/plain; charset=utf-8" }, body: "Текстовая версия" }, { headers: { "content-type": "text/html; charset=utf-8" }, body: "

HTML версия

" } ]); }); it("выбирает text/plain из multipart/alternative перед HTML", () => { const body = [ "--alt", "Content-Type: text/html; charset=utf-8", "Content-Transfer-Encoding: quoted-printable", "", "

=D0=92=D0=B5=D1=80=D1=81=D0=B8=D1=8F HTML

", "--alt", "Content-Type: text/plain; charset=utf-8", "", "Текстовая версия", "--alt--" ].join("\r\n"); expect(extractMessageBody({ "content-type": 'multipart/alternative; boundary="alt"' }, body)).toBe("Текстовая версия"); }); it("преобразует HTML body в читаемый текст, если text/plain отсутствует", () => { const body = [ "--alt", "Content-Type: text/html; charset=utf-8", "", "

Тема

Строка 1
Строка & 2

", "--alt--" ].join("\r\n"); expect(htmlToPlainText("

Строка 1
Строка & 2

")).toBe("Строка 1\nСтрока & 2"); expect(extractMessageBody({ "content-type": "multipart/alternative; boundary=alt" }, body)).toBe("Тема\nСтрока 1\nСтрока & 2\n• Пункт"); }); it("извлекает MIME-вложения из multipart/mixed и не смешивает их с текстом письма", async () => { const body = [ "--mixed", "Content-Type: text/plain; charset=utf-8", "", "Текст письма", "--mixed", "Content-Type: text/plain; name*=utf-8''%D0%BE%D1%82%D1%87%D0%B5%D1%82.txt", "Content-Disposition: attachment; filename*=utf-8''%D0%BE%D1%82%D1%87%D0%B5%D1%82.txt", "Content-Transfer-Encoding: base64", "", "0J/RgNC40LLQtdGC", "--mixed--" ].join("\r\n"); const headers = { "content-type": 'multipart/mixed; boundary="mixed"' }; const attachments = extractMessageAttachments(headers, body); expect(extractMessageBody(headers, body)).toBe("Текст письма"); expect(attachments).toHaveLength(1); expect(attachments[0]).toMatchObject({ fileName: "отчет.txt", contentType: "text/plain", size: new TextEncoder().encode("Привет").byteLength, contentBase64: "0J/RgNC40LLQtdGC", disposition: "attachment" }); await expect(attachmentToBlob(attachments[0]).text()).resolves.toBe("Привет"); }); it("извлекает inline MIME-вложения с Content-ID и не добавляет бинарную часть в тело", async () => { const body = [ "--related", "Content-Type: text/html; charset=utf-8", "", "

Logo

", "--related", "Content-Type: image/png", "Content-Disposition: inline", "Content-ID: ", "Content-Transfer-Encoding: base64", "", "SGVsbG8=", "--related--" ].join("\r\n"); const headers = { "content-type": 'multipart/related; boundary="related"' }; const attachments = extractMessageAttachments(headers, body); expect(extractMessageBody(headers, body)).toBe("Logo"); expect(attachments).toEqual([ { id: "attachment-вложение-1-1", fileName: "Вложение 1", contentType: "image/png", size: 5, contentBase64: "SGVsbG8=", disposition: "inline", contentId: "logo" } ]); await expect(attachmentToBlob(attachments[0]).text()).resolves.toBe("Hello"); }); it("импортирует multipart EML как письмо QOutlook", async () => { const eml = [ "From: sender@example.com", "To: reader@example.com", "Cc: copy@example.com", "Bcc: hidden@example.com", "Reply-To: support@example.com", "Message-ID: ", "In-Reply-To: ", "References: ", "Importance: high", "X-Priority: 1", "Priority: urgent", "Subject: =?UTF-8?B?0JzRg9C70YzRgtC40L/QsNGA0YI=?=", "Date: Mon, 01 Jun 2026 10:00:00 +0300", 'Content-Type: multipart/alternative; boundary="alt"', "", "--alt", "Content-Type: text/html; charset=utf-8", "", "

HTML

", "--alt", "Content-Type: text/plain; charset=utf-8", "Content-Transfer-Encoding: base64", "", "0J/RgNC40LLQtdGCLCDQv9C40YHRjNC80L4=", "--alt--" ].join("\r\n"); const imported = await importEmlFile(new File([eml], "message.eml", { type: "message/rfc822" })); expect(imported.subject).toBe("Мультипарт"); expect(imported.body).toBe("Привет, письмо"); expect(imported.bodyHtml).toBe("

HTML

"); expect(imported.from).toBe("sender@example.com"); expect(imported.to).toBe("reader@example.com"); expect(imported.cc).toBe("copy@example.com"); expect(imported.bcc).toBe("hidden@example.com"); expect(imported.replyTo).toBe("support@example.com"); expect(imported.messageId).toBe(""); expect(imported.inReplyTo).toBe(""); expect(imported.references).toBe(" "); expect(imported.importance).toBe("high"); expect(imported.xPriority).toBe("1"); expect(imported.priority).toBe("urgent"); }); it("импортирует multipart/mixed EML с вложением", async () => { const eml = [ "From: sender@example.com", "To: reader@example.com", "Subject: Отчет", "Date: Mon, 01 Jun 2026 10:00:00 +0300", 'Content-Type: multipart/mixed; boundary="mixed"', "", "--mixed", "Content-Type: text/plain; charset=utf-8", "", "Письмо с файлом", "--mixed", "Content-Type: application/octet-stream; name=\"report.bin\"", "Content-Disposition: attachment; filename=\"report.bin\"", "Content-Transfer-Encoding: base64", "", "SGVsbG8=", "--mixed--" ].join("\r\n"); const imported = await importEmlFile(new File([eml], "message.eml", { type: "message/rfc822" })); expect(imported.body).toBe("Письмо с файлом"); expect(imported.attachments).toEqual([ { id: "attachment-report-bin-1", fileName: "report.bin", contentType: "application/octet-stream", size: 5, contentBase64: "SGVsbG8=", disposition: "attachment" } ]); }); it("преобразует данные Outlook MSG в письмо QOutlook с вложениями", async () => { const message = msgDataToMailMessage( { getAttachment: () => ({ fileName: "report.txt", content: new TextEncoder().encode("Hello") }) }, { senderSmtpAddress: "sender@example.com", subject: "MSG отчет", headers: [ "Reply-To: support@example.com", "Message-ID: ", "In-Reply-To: ", "References: ", "Importance: low", "X-Priority: 5", "Priority: non-urgent", "From: sender@example.com" ].join("\r\n"), bodyHtml: "

HTML текст

", messageDeliveryTime: "Mon, 01 Jun 2026 10:00:00 GMT", recipients: [ { smtpAddress: "reader@example.com", recipType: "to" }, { smtpAddress: "copy@example.com", recipType: "cc" }, { smtpAddress: "hidden@example.com", recipType: "bcc" } ], attachments: [{ fileName: "report.txt", attachMimeTag: "text/plain", contentLength: 5 }] } ); expect(message).toMatchObject({ folder: "inbox", from: "sender@example.com", to: "reader@example.com", cc: "copy@example.com", bcc: "hidden@example.com", replyTo: "support@example.com", messageId: "", inReplyTo: "", references: " ", importance: "low", xPriority: "5", priority: "non-urgent", subject: "MSG отчет", bodyHtml: "

HTML текст

", body: "HTML текст", time: "Mon, 01 Jun 2026 10:00:00 GMT", read: false, flagged: false }); expect(message.bodyHtml).toBe("

HTML текст

"); expect(message.id).toMatch(/^msg-/); expect(message.attachments).toEqual([ { id: "attachment-report-txt-1", fileName: "report.txt", contentType: "text/plain", size: 5, contentBase64: "SGVsbG8=", disposition: "attachment" } ]); await expect(attachmentToBlob(message.attachments?.[0]!).text()).resolves.toBe("Hello"); }); it("собирает EML source с UTF-8 subject encoding", () => { const message: MailMessage = { id: "1", folder: "sent", from: "sender@example.com", to: "reader@example.com", cc: "copy@example.com", bcc: "hidden@example.com", replyTo: "support@example.com", messageId: "", inReplyTo: "", references: " ", importance: "high", xPriority: "1", priority: "urgent", subject: "Тема", body: "Текст", time: "Mon, 01 Jan 2024 10:00:00 +0000", read: true, flagged: false }; const eml = buildEmlSource(message); expect(eml).toContain("From: sender@example.com"); expect(eml).toContain("Cc: copy@example.com"); expect(eml).toContain("Bcc: hidden@example.com"); expect(eml).toContain("Reply-To: support@example.com"); expect(eml).toContain("Message-ID: "); expect(eml).toContain("In-Reply-To: "); expect(eml).toContain("References: "); expect(eml).toContain("Importance: high"); expect(eml).toContain("X-Priority: 1"); expect(eml).toContain("Priority: urgent"); expect(eml).toContain("Subject: =?UTF-8?B?"); expect(eml.endsWith("Текст")).toBe(true); }); it("exports HTML body as multipart/alternative EML", () => { const message: MailMessage = { id: "html-1", folder: "sent", from: "sender@example.com", to: "reader@example.com", subject: "HTML message", body: "Plain text", bodyHtml: "

HTML text

", time: "Mon, 01 Jan 2024 10:00:00 +0000", read: true, flagged: false }; const eml = buildEmlSource(message); const parsed = parseEmlSource(eml); expect(eml).toContain("Content-Type: multipart/alternative"); expect(extractMessageBody(parsed.headers, parsed.body)).toBe("Plain text"); expect(extractMessageHtmlBody(parsed.headers, parsed.body)).toBe("

HTML text

"); }); it("собирает multipart/mixed EML с вложениями и RFC 2231 filename", () => { const message: MailMessage = { id: "1", folder: "sent", from: "sender@example.com", to: "reader@example.com", cc: "copy@example.com", bcc: "hidden@example.com", replyTo: "support@example.com", messageId: "", inReplyTo: "", references: " ", importance: "high", xPriority: "1", priority: "urgent", subject: "Тема", body: "Текст", bodyHtml: "

Текст

", time: "Mon, 01 Jan 2024 10:00:00 +0000", read: true, flagged: false, attachments: [ { id: "a1", fileName: "отчет.txt", contentType: "text/plain", size: 5, contentBase64: "SGVsbG8=", disposition: "attachment" } ] }; const eml = buildEmlSource(message); const parsed = parseEmlSource(eml); const attachments = extractMessageAttachments(parsed.headers, parsed.body); expect(eml).toContain("Content-Type: multipart/mixed"); expect(eml).toContain("Content-Type: multipart/alternative"); expect(eml).toContain("Cc: copy@example.com"); expect(eml).toContain("Bcc: hidden@example.com"); expect(eml).toContain("Reply-To: support@example.com"); expect(eml).toContain("Message-ID: "); expect(eml).toContain("In-Reply-To: "); expect(eml).toContain("References: "); expect(eml).toContain("Importance: high"); expect(eml).toContain("X-Priority: 1"); expect(eml).toContain("Priority: urgent"); expect(eml).toContain("filename*=utf-8''%D0%BE%D1%82%D1%87%D0%B5%D1%82%2E%74%78%74"); expect(extractMessageBody(parsed.headers, parsed.body)).toBe("Текст"); expect(extractMessageHtmlBody(parsed.headers, parsed.body)).toBe("

Текст

"); expect(attachments[0]).toMatchObject({ fileName: "отчет.txt", contentType: "text/plain", contentBase64: "SGVsbG8=", size: 5 }); }); it("создает вложение из File для новых писем", async () => { const attachment = await createMailAttachmentFromFile(new File(["Привет"], "note.txt", { type: "text/plain" })); expect(attachment).toMatchObject({ fileName: "note.txt", contentType: "text/plain", size: new TextEncoder().encode("Привет").byteLength, contentBase64: "0J/RgNC40LLQtdGC", disposition: "attachment" }); expect(attachment.id).toMatch(/^attachment-note-txt-/); }); it("принимает EML и MSG filenames для импорта почты", () => { expect(isSupportedEmlFileName("message.eml")).toBe(true); expect(isSupportedMsgFileName("message.msg")).toBe(true); expect(isSupportedMailFileName("message.eml")).toBe(true); expect(isSupportedMailFileName("message.msg")).toBe(true); expect(isSupportedEmlFileName("archive.pst")).toBe(false); expect(isSupportedMailFileName("archive.pst")).toBe(false); }); });