diff options
Diffstat (limited to 'deno/mail-relay/mail-parsing.ts')
-rw-r--r-- | deno/mail-relay/mail-parsing.ts | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/deno/mail-relay/mail-parsing.ts b/deno/mail-relay/mail-parsing.ts new file mode 100644 index 0000000..7e76257 --- /dev/null +++ b/deno/mail-relay/mail-parsing.ts @@ -0,0 +1,196 @@ +import emailAddresses from "email-addresses"; + +class MailSimpleParseError extends Error {} + +function lazy<T>(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<string>(); + 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); +} |