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); } function isTgzName(name: string): boolean { return /\.(tgz|tar\.gz)$/i.test(name); } function isArchiveName(name: string): boolean { return isZipName(name) || isTgzName(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; } async function gunzip(buffer: ArrayBuffer): Promise { try { const ds = new DecompressionStream("gzip"); const writer = ds.writable.getWriter(); const reader = ds.readable.getReader(); writer.write(new Uint8Array(buffer)); writer.close(); const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); } const totalLength = chunks.reduce((sum, c) => sum + c.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result.buffer; } catch { throw new Error("Failed to decompress gzip archive"); } } interface TarEntry { name: string; type: string; data: ArrayBuffer; } function parseTar(buffer: ArrayBuffer): TarEntry[] { const entries: TarEntry[] = []; const view = new Uint8Array(buffer); let offset = 0; while (offset + 512 <= view.byteLength) { const header = new Uint8Array(buffer, offset, 512); const name = new TextDecoder().decode(header.subarray(0, 100)).replace(/\0.*$/, ""); const type = String.fromCharCode(header[156] || 0) || "0"; if (!name) break; let sizeStr = ""; for (let i = 124; i < 136; i++) { const ch = String.fromCharCode(header[i]); if (ch === "\0" || ch === " ") break; sizeStr += ch; } const size = parseInt(sizeStr || "0", 8); offset += 512; if (size > 0 && type === "0" && isEmlName(name)) { const data = buffer.slice(offset, offset + size); entries.push({ name: name.split(/[\\/]/).pop() || name, type: "file", data, }); } offset += Math.ceil(size / 512) * 512; } return entries; } async function extractEmlsFromTgz(file: File): Promise { const buffer = await file.arrayBuffer(); const decompressed = await gunzip(buffer); const entries = parseTar(decompressed); return entries.map((entry) => ({ name: entry.name, blob: new Blob([entry.data], { type: EMAIL_MIME }), })); } 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; } if (isTgzName(file.name) || file.type === "application/gzip" || file.type === "application/x-gtar") { out.push(...(await extractEmlsFromTgz(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,.tgz,.tar.gz,message/rfc822,application/zip,application/gzip";