Files
webmail/lib/mail-import/pst/items.ts
T
Linus Rath 7102a6f0cb fix: read Outlook files more safely and put their special folders in the right place
- A message whose attachments are bigger than the server takes is
  skipped before anything is read (a 1 GB attachment took several GB in
  the worker and could crash it), and a worker that stops answering
  ends the import with a message instead of a wait that never ends.
- The items after a broken one in a PST folder are counted as
  unreadable in the summary instead of disappearing.
- Sent Items and Deleted Items are known from the PST's own entry IDs,
  and the Inbox and the others by their names in more languages, so a
  PST imported at the top level fills the user's own folders whatever
  its language. The hint about that shows only for the top level.
- Message-ID, In-Reply-To and Content-ID from the PST can't carry line
  breaks into the rebuilt headers.
2026-11-01 06:45:33 +01:00

104 lines
3.5 KiB
TypeScript

import { itemKind, pstFolders, readPstMessage, type PstFolderRef } from "./extract";
import { buildMime } from "./mime";
import type { SkipReason } from "../sources";
/** One PST item as the worker hands it over: a message's MIME, or why it was skipped. */
export type PstItem =
| {
kind: "message";
index: number;
folder: string[];
mime: Uint8Array;
keywords: Record<string, true>;
receivedAt?: string;
label: string;
embeddedSkipped: number;
folderRole?: string;
}
| { kind: "skipped"; index: number; reason: SkipReason; label?: string };
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface PstWalk {
/** Items in the folders that are read (an estimate: Outlook keeps the count). */
total: number;
items: Generator<PstItem>;
}
function utcDate(date: Date | undefined): string | undefined {
if (!date || Number.isNaN(date.getTime())) return undefined;
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
}
export function walkPst(pst: any, options: { skipBefore: number; skipAlso?: Set<number>; maxMessageBytes: number; boundarySeed?: string }): PstWalk {
const folders: PstFolderRef[] = [...pstFolders(pst)];
const total = folders.reduce((n, f) => n + (Number(f.folder.contentCount) || 0), 0);
function* items(): Generator<PstItem> {
let index = 0;
for (const ref of folders) {
if (!(Number(ref.folder.contentCount) > 0)) continue;
const count = Number(ref.folder.contentCount) || 0;
let read = 0;
for (;;) {
let child: any;
try {
child = ref.folder.getNextChild();
} catch {
// A broken item ends this folder's list in pst-extractor: count what is
// left of the folder as unreadable and go on with the next folder.
for (let left = count - read; left > 0; left--) {
const i = index++;
yield i < options.skipBefore || options.skipAlso?.has(i)
? { kind: "skipped", index: i, reason: "resumed" }
: { kind: "skipped", index: i, reason: "unreadable" };
}
break;
}
if (!child) break;
read++;
const i = index++;
if (i < options.skipBefore || options.skipAlso?.has(i)) {
yield { kind: "skipped", index: i, reason: "resumed" };
continue;
}
const kind = itemKind(String(child.messageClass || ""));
const label = String(child.subject || "");
if (kind !== "mail") {
yield { kind: "skipped", index: i, reason: kind, label };
continue;
}
let mime: Uint8Array;
let message;
try {
message = readPstMessage(child, options.maxMessageBytes);
if (message.tooLarge) {
yield { kind: "skipped", index: i, reason: "too_large", label };
continue;
}
mime = buildMime(message.mail, { boundarySeed: options.boundarySeed });
} catch {
yield { kind: "skipped", index: i, reason: "other", label };
continue;
}
if (mime.length > options.maxMessageBytes) {
yield { kind: "skipped", index: i, reason: "too_large", label };
continue;
}
yield {
kind: "message",
index: i,
folder: ref.path,
mime,
keywords: message.keywords,
receivedAt: utcDate(message.receivedAt),
label,
embeddedSkipped: message.embedded,
folderRole: ref.topRole,
};
}
}
}
return { total, items: items() };
}