import emailAddresses from "email-addresses"; class MailSimpleParseError extends Error {} function lazy(calculator: () => T): () => T { const tag = new Object(); let cache: typeof tag | T = tag; return () => { if (cache === tag) cache = calculator(); return cache as T; }; } class MailSimpleParsedHeaders { #headerSection; #headers = lazy(() => { const headers = [] as [key: string, value: string][]; let field: string | null = null; let lineNumber = 1; const handleField = () => { if (field == null) return; const sepPos = field.indexOf(":"); if (sepPos === -1) { throw new MailSimpleParseError(`No ':' in the header line: ${field}`); } headers.push([field.slice(0, sepPos).trim(), field.slice(sepPos + 1)]); field = null; }; for (const line of this.#headerSection.trimEnd().split(/\r?\n|\r/)) { if (line.match(/^\s/)) { if (field == null) { throw new MailSimpleParseError("Header section starts with a space."); } field += line; } else { handleField(); field = line; } lineNumber += 1; } handleField(); return headers; }); #messageId = lazy(() => { const messageIdField = this.#getFirst("message-id"); if (messageIdField == null) return undefined; const match = messageIdField.match(/\<(.*?)\>/); if (match != null) { return match[1]; } else { console.warn("Invalid message-id header of mail: " + messageIdField); return undefined; } }); #date = lazy(() => { const dateField = this.#getFirst("date"); if (dateField == null) return undefined; const date = new Date(dateField); if (isNaN(date.getTime())) { console.warn(`Invalid date string (${dateField}) found in header.`); return undefined; } return date; }); #from = lazy(() => { const fromField = this.#getFirst("from"); if (fromField == null) return undefined; const addr = emailAddresses.parseOneAddress(fromField); return addr?.type === "mailbox" ? addr.address : undefined; }); #recipients = lazy(() => { const headers = ["to", "cc", "bcc", "x-original-to"]; const recipients = new Set(); for (const [key, value] of this.#headers()) { if (headers.includes(key.toLowerCase())) { emailAddresses .parseAddressList(value) ?.flatMap((a) => (a.type === "mailbox" ? a : a.addresses)) ?.forEach(({ address }) => recipients.add(address)); } } return recipients; }); constructor(headerSection: string) { this.#headerSection = headerSection; } #getFirst(fieldKey: string): string | undefined { for (const [key, value] of this.#headers()) { if (key.toLowerCase() === fieldKey.toLowerCase()) return value; } return undefined; } get messageId() { return this.#messageId(); } get date() { return this.#date(); } get from() { return this.#from(); } get recipients() { return this.#recipients(); } toList(): [string, string][] { return [...this.#headers()]; } } class MailSimpleParsed { #raw; #sections = lazy(() => { const twoEolMatch = this.#raw.match(/(\r?\n)(\r?\n)/); if (twoEolMatch == null) { throw new MailSimpleParseError( "No header/body section separator (2 successive EOLs) found.", ); } const [eol, sep] = [twoEolMatch[1], twoEolMatch[2]]; if (eol !== sep) { console.warn("Different EOLs (\\r\\n, \\n) found."); } return { header: this.#raw.slice(0, twoEolMatch.index!), body: this.#raw.slice(twoEolMatch.index! + eol.length + sep.length), eol, sep, }; }); #headers = lazy(() => { return new MailSimpleParsedHeaders(this.header); }); constructor(raw: string) { this.#raw = raw; } get header() { return this.#sections().header; } get body() { return this.#sections().body; } get sep() { return this.#sections().sep; } get eol() { return this.#sections().eol; } get headers() { return this.#headers(); } get date() { return this.headers.date; } get messageId() { return this.headers.messageId; } get from() { return this.headers.from; } get recipients() { return this.headers.recipients; } } export function simpleParseMail(raw: string): MailSimpleParsed { return new MailSimpleParsed(raw); }