Files
Bernd Rodler b98ab59f0d fix: Phase 2 QA — all 25 remaining HIGH/MEDIUM/LOW issues
HIGH fixes (7):
- H1: VNCdirectory admin i18n — 30+ translation keys added
- H2: handleSave try/catch with error toast
- H3: Free/busy accountId scoping
- H4: cancelEventBookings filter by eventId
- H5: Resource picker static apiFetch import
- H6: Sharing-store toast messages via lastMessage state
- H7: roleLabel for all resource types

MEDIUM fixes (11):
- M1: identitySignatureMap cleanup on delete
- M2: Now-line relative positioning
- M3: Radial menu disabled item keyboard nav
- M4: Radial menu stable event listener via refs
- M5: cancelBooking error on missing booking
- M6: PasswordRow isMasked state flag
- M7: Extract shared rights into lib/sharing-rights.ts
- M8: VNCtalk client server-side guard
- M9: Collabora configManager instead of process.env
- M10: CONFIG_ENV_MAP VNCdirectory fields
- M11: SENSITIVE_CONFIG_KEYS field name unification

LOW fixes (7):
- L1-L3: Unused imports removed
- L4: aria-labels on close, clear, search, spinner
- L5-L7: Comments for intentional patterns, null guard
2026-08-07 14:21:07 +02:00

139 lines
3.7 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);
}
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";