- P2.3: Folder sharing system — ShareFolderDialog, sharing-store, sharing-settings - P2.5: Email import (.eml, .tgz, .zip) with dedup and progress - P2.6: Contact import (vCard + CSV) with auto-mapping - P2.7: Free/Busy view grid with color-coded slots
143 lines
3.8 KiB
TypeScript
143 lines
3.8 KiB
TypeScript
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<ImportableEmail[]> {
|
|
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<ArrayBuffer> {
|
|
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<ImportableEmail[]> {
|
|
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<ImportableEmail[]> {
|
|
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";
|