Preserve QOutlook inline EML attachments
This commit is contained in:
@@ -34,7 +34,7 @@ npm run build
|
||||
- QWord открывает `.docx` и сохраняет `.docx`; при импорте базовых DOCX-абзацев читает выравнивание, внешние гиперссылки, простые таблицы, базовые растровые изображения, явные цвета текста, подсветку, размер и семейство шрифта из OOXML, а при экспорте DOCX сохраняются заголовки, абзацы, выравнивание абзацев, списки, простые таблицы, внешние гиперссылки, базовые растровые изображения и базовое inline-форматирование текста: полужирный, курсив, подчеркивание, зачеркивание, цвет текста, подсветка, размер и семейство шрифта.
|
||||
- QExcell открывает `.xlsx` и сохраняет `.xlsx`; сетка расширяется по фактически заполненным ячейкам, поддерживает несколько листов, формулы за пределами стартового диапазона A-H, shared formulas XLSX с относительными ссылками, базовые merged cells, внешние и внутренние гиперссылки ячеек, базовые диаграммы листа и базовое форматирование ячеек: полужирный, курсив, подчеркивание, зачеркивание, размер и семейство шрифта, цвет текста, заливку, горизонтальное и вертикальное выравнивание, перенос текста, границы и цвет границ ячеек, числовые форматы XLSX для дат, процентов, денежных и дробных значений.
|
||||
- QPowerPoint открывает `.pptx` и сохраняет `.pptx`; импорт учитывает порядок слайдов из `presentation.xml`, переносы абзацев внутри текстовых блоков и заметки докладчика через relationships слайда, сохраняет заметки докладчика, базовые растровые изображения слайдов, пользовательский цвет фона слайда, а также цвет, размер и семейство шрифта текста заголовка/подзаголовка.
|
||||
- QOutlook открывает `.eml` и одиночные сообщения Outlook `.msg`, сохраняет письма в `.eml`; импорт EML поддерживает простые текстовые письма, `multipart/alternative`, `multipart/mixed`, `text/plain`, fallback из `text/html` в читаемый текст, base64, quoted-printable, MIME encoded-word заголовки UTF-8/windows-1251, MIME-вложения, `To`, `Cc` и `Bcc`. Импорт MSG читает тему, отправителя, получателей `To/Cc/Bcc`, текст/HTML body и вложения, если эти поля доступны в MSG-файле. Новые письма можно адресовать через `To/Cc/Bcc` и сохранять в EML вместе с добавленными файлами. `.pst` и экспорт в `.msg` пока не реализованы.
|
||||
- QOutlook открывает `.eml` и одиночные сообщения Outlook `.msg`, сохраняет письма в `.eml`; импорт EML поддерживает простые текстовые письма, `multipart/alternative`, `multipart/mixed`, `multipart/related`, `text/plain`, fallback из `text/html` в читаемый текст, base64, quoted-printable, MIME encoded-word заголовки UTF-8/windows-1251, MIME-вложения и inline-вложения с `Content-ID`, `To`, `Cc` и `Bcc`. Импорт MSG читает тему, отправителя, получателей `To/Cc/Bcc`, текст/HTML body и вложения, если эти поля доступны в MSG-файле. Новые письма можно адресовать через `To/Cc/Bcc` и сохранять в EML вместе с добавленными файлами. `.pst` и экспорт в `.msg` пока не реализованы.
|
||||
|
||||
В desktop-режиме Electron используются системные диалоги открытия и сохранения файлов. В web-режиме остаются браузерный выбор файла и скачивание результата.
|
||||
Экспорт PDF в desktop-режиме выполняется через Electron `webContents.printToPDF`; в web-режиме команда открывает браузерную печать, где пользователь может выбрать сохранение в PDF.
|
||||
@@ -46,7 +46,7 @@ Desktop-меню поддерживает системные команды дл
|
||||
- `Ctrl/Cmd+Shift+E`: экспортировать текущую рабочую область в PDF.
|
||||
- `Ctrl/Cmd+P`: печать текущей рабочей области.
|
||||
|
||||
Импорт и экспорт реализуют базовую совместимость с Office Open XML, EML и чтением MSG: текст, базовое inline-форматирование, явные `w:color`, `w:shd`, `w:highlight`, `w:sz`, `w:rFonts` и `w:strike` для цвета/подсветки/размера/семейства/зачеркивания текста DOCX, выравнивание абзацев DOCX, внешние гиперссылки DOCX, внешние и внутренние гиперссылки XLSX, базовые растровые изображения DOCX/PPTX, простые таблицы, несколько листов XLSX, ячейки, формулы, shared formulas XLSX с относительными ссылками, базовые merged cells, базовые стили, цвета текста, размеры и семейства шрифта, зачеркивание ячеек, горизонтальное и вертикальное выравнивание, перенос текста, границы и цвет границ ячеек, числовые форматы и простые bar/line/pie диаграммы ячеек XLSX, текстовые слайды PPTX с переносами абзацев при импорте, пользовательский фон слайдов PPTX, цвет, размер и семейство шрифта текста заголовка/подзаголовка PPTX, импорт и экспорт заметок докладчика, адресацию `To/Cc/Bcc`, MIME encoded-word заголовки UTF-8/windows-1251, MIME-вложения EML и базовые поля одиночных MSG-писем. Сложные стили, макросы, сложные диаграммы, сложное позиционирование и обтекание изображений, PST, экспорт MSG и полная fidelity документов Microsoft Office в текущем инкременте не заявлены.
|
||||
Импорт и экспорт реализуют базовую совместимость с Office Open XML, EML и чтением MSG: текст, базовое inline-форматирование, явные `w:color`, `w:shd`, `w:highlight`, `w:sz`, `w:rFonts` и `w:strike` для цвета/подсветки/размера/семейства/зачеркивания текста DOCX, выравнивание абзацев DOCX, внешние гиперссылки DOCX, внешние и внутренние гиперссылки XLSX, базовые растровые изображения DOCX/PPTX, простые таблицы, несколько листов XLSX, ячейки, формулы, shared formulas XLSX с относительными ссылками, базовые merged cells, базовые стили, цвета текста, размеры и семейства шрифта, зачеркивание ячеек, горизонтальное и вертикальное выравнивание, перенос текста, границы и цвет границ ячеек, числовые форматы и простые bar/line/pie диаграммы ячеек XLSX, текстовые слайды PPTX с переносами абзацев при импорте, пользовательский фон слайдов PPTX, цвет, размер и семейство шрифта текста заголовка/подзаголовка PPTX, импорт и экспорт заметок докладчика, адресацию `To/Cc/Bcc`, MIME encoded-word заголовки UTF-8/windows-1251, MIME-вложения EML, inline-вложения EML с `Content-ID` и базовые поля одиночных MSG-писем. Сложные стили, макросы, сложные диаграммы, сложное позиционирование и обтекание изображений, PST, экспорт MSG и полная fidelity документов Microsoft Office в текущем инкременте не заявлены.
|
||||
|
||||
## Сборка пакета
|
||||
|
||||
|
||||
@@ -137,6 +137,39 @@ describe("outlookMail helpers", () => {
|
||||
await expect(attachmentToBlob(attachments[0]).text()).resolves.toBe("Привет");
|
||||
});
|
||||
|
||||
it("извлекает inline MIME-вложения с Content-ID и не добавляет бинарную часть в тело", async () => {
|
||||
const body = [
|
||||
"--related",
|
||||
"Content-Type: text/html; charset=utf-8",
|
||||
"",
|
||||
"<p>Logo</p><img src=\"cid:logo\">",
|
||||
"--related",
|
||||
"Content-Type: image/png",
|
||||
"Content-Disposition: inline",
|
||||
"Content-ID: <logo>",
|
||||
"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",
|
||||
|
||||
@@ -200,6 +200,7 @@ export function extractMessageBody(headers: MailHeaders, body: string): string {
|
||||
|
||||
const bodyParts = parseMultipartBody(body, boundary)
|
||||
.filter((part) => !isAttachmentPart(part.headers))
|
||||
.filter((part) => isMessageBodyPart(part.headers))
|
||||
.map((part) => {
|
||||
const partType = parseContentType(part.headers["content-type"]);
|
||||
return {
|
||||
@@ -718,13 +719,20 @@ function unquoteHeaderValue(value: string): string {
|
||||
function isAttachmentPart(headers: MailHeaders): boolean {
|
||||
const disposition = parseContentDisposition(headers["content-disposition"]);
|
||||
const contentType = parseContentType(headers["content-type"]);
|
||||
const dispositionValue = disposition.value.toLowerCase();
|
||||
return (
|
||||
disposition.value.toLowerCase() === "attachment" ||
|
||||
dispositionValue === "attachment" ||
|
||||
(dispositionValue === "inline" && Boolean(headers["content-id"])) ||
|
||||
Boolean(decodeMimeParameter(disposition.parameters, "filename")) ||
|
||||
Boolean(decodeMimeParameter(contentType.parameters, "name"))
|
||||
);
|
||||
}
|
||||
|
||||
function isMessageBodyPart(headers: MailHeaders): boolean {
|
||||
const contentType = parseContentType(headers["content-type"]);
|
||||
return contentType.mediaType.startsWith("multipart/") || contentType.mediaType === "text/plain" || contentType.mediaType === "text/html";
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(value: string): string {
|
||||
const namedEntities: Record<string, string> = {
|
||||
amp: "&",
|
||||
|
||||
Reference in New Issue
Block a user