diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index c09a63ea..aa22ae36 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -57,6 +57,7 @@ import { FilePreviewModal } from "@/components/files/file-preview-modal"; import { isFilePreviewable } from "@/lib/file-preview"; import { appendPlainTextSignature } from "@/lib/signature-utils"; import { computeReplyThreadingHeaders } from "@/lib/email-threading"; +import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import"; import { resolveReplyFrom } from "@/lib/reply-identity"; import { Search, Filter, ChevronDown, X, Paperclip, Star, Mail, MailOpen, RotateCcw, PenSquare, PenLine, CheckSquare, Square, AlertTriangle } from "lucide-react"; import { ResizeHandle } from "@/components/layout/resize-handle"; @@ -1753,17 +1754,24 @@ export default function Home() { const input = document.createElement('input'); input.type = 'file'; - input.accept = '.eml,message/rfc822'; + input.accept = EML_IMPORT_ACCEPT; input.multiple = true; input.onchange = async (e) => { const files = Array.from((e.target as HTMLInputElement).files ?? []); if (files.length === 0) return; + let emails; + try { + emails = await expandImportableEmails(files); + } catch { + toast.error(t('notifications.import_email_error')); + return; + } + let imported = 0; let failed = 0; - for (const file of files) { + for (const { blob } of emails) { try { - const blob = new Blob([await file.arrayBuffer()], { type: 'message/rfc822' }); await client.importRawEmail(blob, { [targetMailboxId]: true }, { '$seen': true }); imported++; } catch { @@ -1775,7 +1783,7 @@ export default function Home() { toast.success(t('notifications.import_email_success')); if (selectedMailbox) await fetchEmails(client, selectedMailbox); } - if (failed > 0) { + if (failed > 0 || (imported === 0 && emails.length === 0)) { toast.error(t('notifications.import_email_error')); } }; diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 8ff180b1..1ddc9967 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useLayoutEffect, useMemo, useRef, useCallback } fr import DOMPurify from "dompurify"; import { Email, ContactCard, Mailbox } from "@/lib/jmap/types"; import { emailExportFilename, attachmentDownloadFilename, DEFAULT_EMAIL_TEMPLATE, DEFAULT_ATTACHMENT_TEMPLATE } from "@/lib/download-filename"; +import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import"; import { EMAIL_IFRAME_SANITIZE_CONFIG, collapseBlockedImageContainers, escapeHtml, plainTextToSafeHtml, sanitizeEmailHtml, sanitizePlainTextRenderedHtml } from "@/lib/email-sanitization"; import { hasMeaningfulHtmlBody } from "@/lib/signature-utils"; import { Button } from "@/components/ui/button"; @@ -3096,28 +3097,48 @@ export function EmailViewer({ } }; - // Import email from .eml file + // Import email from .eml file or .zip archive containing .eml files const handleImportEmail = () => { if (!client) return; const input = document.createElement('input'); input.type = 'file'; - input.accept = '.eml,message/rfc822'; + input.accept = EML_IMPORT_ACCEPT; + input.multiple = true; input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (!file) return; + const files = Array.from((e.target as HTMLInputElement).files ?? []); + if (files.length === 0) return; + const { selectedMailbox, mailboxes, fetchEmails } = useEmailStore.getState(); + const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); + const mailboxId = mailbox?.originalId || selectedMailbox; + if (!mailboxId) { + toast.error(tNotifications('import_email_error')); + return; + } + + let emails; try { - const { selectedMailbox, mailboxes, fetchEmails } = useEmailStore.getState(); - const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); - const mailboxId = mailbox?.originalId || selectedMailbox; - if (!mailboxId) { - toast.error(tNotifications('import_email_error')); - return; + emails = await expandImportableEmails(files); + } catch { + toast.error(tNotifications('import_email_error')); + return; + } + + let imported = 0; + let failed = 0; + for (const { blob } of emails) { + try { + await client.importRawEmail(blob, { [mailboxId]: true }, { '$seen': true }); + imported++; + } catch { + failed++; } - const blob = new Blob([await file.arrayBuffer()], { type: 'message/rfc822' }); - await client.importRawEmail(blob, { [mailboxId]: true }, { '$seen': true }); + } + + if (imported > 0) { toast.success(tNotifications('import_email_success')); await fetchEmails(client); - } catch { + } + if (failed > 0 || emails.length === 0) { toast.error(tNotifications('import_email_error')); } }; diff --git a/lib/eml-import.ts b/lib/eml-import.ts new file mode 100644 index 00000000..3056cd35 --- /dev/null +++ b/lib/eml-import.ts @@ -0,0 +1,48 @@ +export interface ImportableEmail { + name: string; + blob: Blob; +} + +const EMAIL_MIME = "message/rfc822"; + +function isEmlName(name: string): boolean { + return /\.eml$/i.test(name); +} + +function isZipName(name: string): boolean { + return /\.zip$/i.test(name); +} + +async function extractEmlsFromZip(file: File): Promise { + const { default: JSZip } = await import("jszip"); + const zip = await JSZip.loadAsync(await file.arrayBuffer()); + const out: ImportableEmail[] = []; + const entries = Object.values(zip.files); + for (const entry of entries) { + if (entry.dir) continue; + if (!isEmlName(entry.name)) continue; + const data = await entry.async("arraybuffer"); + out.push({ + name: entry.name.split(/[\\/]/).pop() || entry.name, + blob: new Blob([data], { type: EMAIL_MIME }), + }); + } + return out; +} + +export async function expandImportableEmails( + files: File[], +): Promise { + const out: ImportableEmail[] = []; + for (const file of files) { + if (isZipName(file.name) || file.type === "application/zip") { + out.push(...(await extractEmlsFromZip(file))); + continue; + } + const blob = new Blob([await file.arrayBuffer()], { type: EMAIL_MIME }); + out.push({ name: file.name, blob }); + } + return out; +} + +export const EML_IMPORT_ACCEPT = ".eml,.zip,message/rfc822,application/zip"; diff --git a/locales/cs/common.json b/locales/cs/common.json index 0ec32e1a..bf03e236 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -270,7 +270,7 @@ "print": "Tisk", "view_source": "Zobrazit zdrojový kód", "export_email": "Exportovat jako .eml", - "import_email": "Importovat .eml", + "import_email": "Importovat .eml nebo .zip", "keyboard_shortcuts": "Klávesové zkratky (?)", "email_source": "Zdrojový kód zprávy", "draft_banner": "Tato zpráva je koncept", @@ -1794,7 +1794,7 @@ "new_subfolder": "Nová podsložka...", "new_folder": "Nová složka...", "rename": "Přejmenovat...", - "import_email": "Importovat .eml...", + "import_email": "Importovat .eml nebo .zip...", "empty_folder": "Vyprázdnit složku", "empty_folder_generic": "Vyprázdnit složku", "delete_folder": "Smazat složku", diff --git a/locales/da/common.json b/locales/da/common.json index 0cd96d6b..9a830b0b 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -270,7 +270,7 @@ "print": "Udskriv", "view_source": "Vis kilde", "export_email": "Eksportér som .eml", - "import_email": "Importér .eml", + "import_email": "Importér .eml eller .zip", "keyboard_shortcuts": "Tastaturgenveje (?)", "email_source": "E-mail-kilde", "draft_banner": "Denne besked er en kladde", @@ -1794,7 +1794,7 @@ "new_subfolder": "Ny undermappe...", "new_folder": "Ny mappe...", "rename": "Omdøb...", - "import_email": "Importér .eml...", + "import_email": "Importér .eml eller .zip...", "empty_folder": "Tøm mappe", "empty_folder_generic": "Tøm mappe", "delete_folder": "Slet mappe", diff --git a/locales/de/common.json b/locales/de/common.json index 9b7fe7fd..ee129229 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -270,7 +270,7 @@ "print": "Drucken", "view_source": "Quelltext anzeigen", "export_email": "Als .eml exportieren", - "import_email": ".eml importieren", + "import_email": ".eml oder .zip importieren", "keyboard_shortcuts": "Tastaturkürzel (?)", "email_source": "E-Mail-Quelltext", "draft_banner": "Diese Nachricht ist ein Entwurf", @@ -1794,7 +1794,7 @@ "new_subfolder": "Neuer Unterordner...", "new_folder": "Neuer Ordner...", "rename": "Umbenennen...", - "import_email": ".eml importieren...", + "import_email": ".eml oder .zip importieren...", "empty_folder": "Ordner leeren", "empty_folder_generic": "Ordner leeren", "delete_folder": "Ordner löschen", diff --git a/locales/en/common.json b/locales/en/common.json index 02143e5a..8d1b9e32 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -270,7 +270,7 @@ "print": "Print", "view_source": "View source", "export_email": "Export as .eml", - "import_email": "Import .eml", + "import_email": "Import .eml or .zip", "keyboard_shortcuts": "Keyboard shortcuts (?)", "email_source": "Email Source", "draft_banner": "This message is a draft", @@ -1794,7 +1794,7 @@ "new_subfolder": "New subfolder...", "new_folder": "New folder...", "rename": "Rename...", - "import_email": "Import .eml...", + "import_email": "Import .eml or .zip...", "empty_folder": "Empty folder", "empty_folder_generic": "Empty folder", "delete_folder": "Delete folder", diff --git a/locales/es/common.json b/locales/es/common.json index 5377bc47..d2fcac44 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -270,7 +270,7 @@ "print": "Imprimir", "view_source": "Ver código fuente", "export_email": "Exportar como .eml", - "import_email": "Importar .eml", + "import_email": "Importar .eml o .zip", "keyboard_shortcuts": "Atajos de teclado (?)", "email_source": "Código Fuente del Correo", "draft_banner": "Este mensaje es un borrador", @@ -1794,7 +1794,7 @@ "new_subfolder": "Nueva subcarpeta...", "new_folder": "Nueva carpeta...", "rename": "Renombrar...", - "import_email": "Importar .eml...", + "import_email": "Importar .eml o .zip...", "empty_folder": "Vaciar carpeta", "empty_folder_generic": "Vaciar carpeta", "delete_folder": "Eliminar carpeta", diff --git a/locales/fr/common.json b/locales/fr/common.json index 87265b9a..2970c893 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -270,7 +270,7 @@ "print": "Imprimer", "view_source": "Voir la source", "export_email": "Exporter en .eml", - "import_email": "Importer un .eml", + "import_email": "Importer .eml ou .zip", "keyboard_shortcuts": "Raccourcis clavier (?)", "email_source": "Source de l'email", "draft_banner": "Ce message est un brouillon", @@ -1794,7 +1794,7 @@ "new_subfolder": "Nouveau sous-dossier...", "new_folder": "Nouveau dossier...", "rename": "Renommer...", - "import_email": "Importer un .eml...", + "import_email": "Importer .eml ou .zip...", "empty_folder": "Vider le dossier", "empty_folder_generic": "Vider le dossier", "delete_folder": "Supprimer le dossier", diff --git a/locales/it/common.json b/locales/it/common.json index 4c92711a..27736557 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -270,7 +270,7 @@ "print": "Stampa", "view_source": "Visualizza sorgente", "export_email": "Esporta come .eml", - "import_email": "Importa .eml", + "import_email": "Importa .eml o .zip", "keyboard_shortcuts": "Scorciatoie da tastiera (?)", "email_source": "Sorgente del messaggio", "draft_banner": "Questo messaggio è una bozza", @@ -1794,7 +1794,7 @@ "new_subfolder": "Nuova sottocartella...", "new_folder": "Nuova cartella...", "rename": "Rinomina...", - "import_email": "Importa .eml...", + "import_email": "Importa .eml o .zip...", "empty_folder": "Svuota cartella", "empty_folder_generic": "Svuota cartella", "delete_folder": "Elimina cartella", diff --git a/locales/ja/common.json b/locales/ja/common.json index ed129cf9..5c8447c3 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -270,7 +270,7 @@ "print": "印刷", "view_source": "ソースを表示", "export_email": ".emlとしてエクスポート", - "import_email": ".emlをインポート", + "import_email": ".eml または .zip をインポート", "keyboard_shortcuts": "キーボードショートカット (?)", "email_source": "メールソース", "draft_banner": "このメッセージは下書きです", @@ -1794,7 +1794,7 @@ "new_subfolder": "新しいサブフォルダー...", "new_folder": "新しいフォルダー...", "rename": "名前を変更...", - "import_email": ".eml をインポート...", + "import_email": ".eml または .zip をインポート...", "empty_folder": "フォルダーを空にする", "empty_folder_generic": "フォルダーを空にする", "delete_folder": "フォルダーを削除", diff --git a/locales/ko/common.json b/locales/ko/common.json index a0cc0dbe..959f2129 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -270,7 +270,7 @@ "print": "인쇄", "view_source": "원본 보기", "export_email": ".eml 파일로 내보내기", - "import_email": ".eml 파일 가져오기", + "import_email": ".eml 또는 .zip 가져오기", "keyboard_shortcuts": "단축키 (?)", "email_source": "메일 원본", "draft_banner": "작성 중인 임시보관 메일이에요", @@ -1794,7 +1794,7 @@ "new_subfolder": "새 하위 폴더...", "new_folder": "새 폴더...", "rename": "이름 바꾸기...", - "import_email": ".eml 가져오기...", + "import_email": ".eml 또는 .zip 가져오기...", "empty_folder": "폴더 비우기", "empty_folder_generic": "폴더 비우기", "delete_folder": "폴더 삭제", diff --git a/locales/lv/common.json b/locales/lv/common.json index 3a1bd542..c259d1c3 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -270,7 +270,7 @@ "print": "Drukāt", "view_source": "Skatīt avota kodu", "export_email": "Eksportēt kā .eml", - "import_email": "Importēt .eml", + "import_email": "Importēt .eml vai .zip", "keyboard_shortcuts": "Īsinājumtaustiņi (?)", "email_source": "Vēstules avota kods", "draft_banner": "Šī vēstule ir melnraksts", @@ -1794,7 +1794,7 @@ "new_subfolder": "Jauna apakšmape...", "new_folder": "Jauna mape...", "rename": "Pārsaukt...", - "import_email": "Importēt .eml...", + "import_email": "Importēt .eml vai .zip...", "empty_folder": "Iztukšot mapi", "empty_folder_generic": "Iztukšot mapi", "delete_folder": "Dzēst mapi", diff --git a/locales/nl/common.json b/locales/nl/common.json index 46df5414..cdb90633 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -270,7 +270,7 @@ "print": "Afdrukken", "view_source": "Bron bekijken", "export_email": "Exporteren als .eml", - "import_email": ".eml importeren", + "import_email": ".eml of .zip importeren", "keyboard_shortcuts": "Sneltoetsen (?)", "email_source": "E-mailbron", "draft_banner": "Dit bericht is een concept", @@ -1794,7 +1794,7 @@ "new_subfolder": "Nieuwe submap...", "new_folder": "Nieuwe map...", "rename": "Hernoemen...", - "import_email": ".eml importeren...", + "import_email": ".eml of .zip importeren...", "empty_folder": "Map leegmaken", "empty_folder_generic": "Map leegmaken", "delete_folder": "Map verwijderen", diff --git a/locales/pl/common.json b/locales/pl/common.json index 434b6023..0e3296e6 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -270,7 +270,7 @@ "print": "Drukuj", "view_source": "Pokaż źródło", "export_email": "Eksportuj jako .eml", - "import_email": "Importuj .eml", + "import_email": "Importuj .eml lub .zip", "keyboard_shortcuts": "Skróty klawiszowe (?)", "email_source": "Źródło wiadomości", "draft_banner": "Ta wiadomość jest szkicem", @@ -1794,7 +1794,7 @@ "new_subfolder": "Nowy podfolder...", "new_folder": "Nowy folder...", "rename": "Zmień nazwę...", - "import_email": "Importuj .eml...", + "import_email": "Importuj .eml lub .zip...", "empty_folder": "Opróżnij folder", "empty_folder_generic": "Opróżnij folder", "delete_folder": "Usuń folder", diff --git a/locales/pt/common.json b/locales/pt/common.json index 57aec53e..afa9df27 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -270,7 +270,7 @@ "print": "Imprimir", "view_source": "Ver código-fonte", "export_email": "Exportar como .eml", - "import_email": "Importar .eml", + "import_email": "Importar .eml ou .zip", "keyboard_shortcuts": "Atalhos de teclado (?)", "email_source": "Código-fonte do E-mail", "draft_banner": "Esta mensagem é um rascunho", @@ -1794,7 +1794,7 @@ "new_subfolder": "Nova subpasta...", "new_folder": "Nova pasta...", "rename": "Renomear...", - "import_email": "Importar .eml...", + "import_email": "Importar .eml ou .zip...", "empty_folder": "Esvaziar pasta", "empty_folder_generic": "Esvaziar pasta", "delete_folder": "Excluir pasta", diff --git a/locales/ru/common.json b/locales/ru/common.json index 3ebb2de1..30a87474 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -270,7 +270,7 @@ "print": "Распечатать", "view_source": "Просмотреть исходный код", "export_email": "Экспортировать как .eml", - "import_email": "Импортировать .eml", + "import_email": "Импортировать .eml или .zip", "keyboard_shortcuts": "Сочетания клавиш (?)", "email_source": "Исходный код письма", "draft_banner": "Это письмо является черновиком", @@ -1794,7 +1794,7 @@ "new_subfolder": "Новая вложенная папка...", "new_folder": "Новая папка...", "rename": "Переименовать...", - "import_email": "Импортировать .eml...", + "import_email": "Импортировать .eml или .zip...", "empty_folder": "Очистить папку", "empty_folder_generic": "Очистить папку", "delete_folder": "Удалить папку", diff --git a/locales/tr/common.json b/locales/tr/common.json index b8bdbffa..af5f1a1c 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -270,7 +270,7 @@ "print": "Yazdır", "view_source": "Kaynağı görüntüle", "export_email": ".eml olarak dışa aktar", - "import_email": ".eml içe aktar", + "import_email": ".eml veya .zip içe aktar", "keyboard_shortcuts": "Klavye kısayolları (?)", "email_source": "E-posta Kaynağı", "draft_banner": "Bu ileti bir taslaktır", @@ -1794,7 +1794,7 @@ "new_subfolder": "Yeni alt klasör...", "new_folder": "Yeni klasör...", "rename": "Yeniden adlandır...", - "import_email": ".eml içe aktar...", + "import_email": ".eml veya .zip içe aktar...", "empty_folder": "Klasörü boşalt", "empty_folder_generic": "Klasörü boşalt", "delete_folder": "Klasörü sil", diff --git a/locales/uk/common.json b/locales/uk/common.json index f3244eec..3ca5c529 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -270,7 +270,7 @@ "print": "Роздрукувати", "view_source": "Переглянути джерело", "export_email": "Експортувати як .eml", - "import_email": "Імпорт .eml", + "import_email": "Імпорт .eml або .zip", "keyboard_shortcuts": "Комбінації клавіш (?)", "email_source": "Джерело електронної пошти", "draft_banner": "Це повідомлення є чернеткою", @@ -1794,7 +1794,7 @@ "new_subfolder": "Нова вкладена папка...", "new_folder": "Нова папка...", "rename": "Перейменувати...", - "import_email": "Імпортувати .eml...", + "import_email": "Імпортувати .eml або .zip...", "empty_folder": "Очистити папку", "empty_folder_generic": "Очистити папку", "delete_folder": "Видалити папку", diff --git a/locales/zh/common.json b/locales/zh/common.json index 1b27b85b..4051e48f 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -270,7 +270,7 @@ "print": "打印", "view_source": "查看源码", "export_email": "导出为 .eml", - "import_email": "导入 .eml", + "import_email": "导入 .eml 或 .zip", "keyboard_shortcuts": "键盘快捷键(?)", "email_source": "邮件源码", "draft_banner": "这是一封草稿邮件", @@ -1794,7 +1794,7 @@ "new_subfolder": "新建子文件夹...", "new_folder": "新建文件夹...", "rename": "重命名...", - "import_email": "导入 .eml...", + "import_email": "导入 .eml 或 .zip...", "empty_folder": "清空文件夹", "empty_folder_generic": "清空文件夹", "delete_folder": "删除文件夹",