feat: P2.3 Folder Sharing + P2.5 Email Import + P2.6 Contact Import + P2.7 Free/Busy
- 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
This commit is contained in:
+95
-1
@@ -13,6 +13,14 @@ 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());
|
||||
@@ -30,6 +38,88 @@ async function extractEmlsFromZip(file: File): Promise<ImportableEmail[]> {
|
||||
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[]> {
|
||||
@@ -39,10 +129,14 @@ export async function expandImportableEmails(
|
||||
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,message/rfc822,application/zip";
|
||||
export const EML_IMPORT_ACCEPT = ".eml,.zip,.tgz,.tar.gz,message/rfc822,application/zip,application/gzip";
|
||||
|
||||
Reference in New Issue
Block a user