diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-06-17 19:04:16 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-06-18 13:26:28 +0800 |
commit | 03082a13dfeb92d3b3d605225937bea1ef2901fc (patch) | |
tree | f37144b9a8824de93029c705bd83257e6a0b9fd0 /deno/mail-relay/mail-parsing.ts | |
parent | 0824de3bae3550674a9ea029b03c5cb8a35cd8e1 (diff) | |
download | crupest-03082a13dfeb92d3b3d605225937bea1ef2901fc.tar.gz crupest-03082a13dfeb92d3b3d605225937bea1ef2901fc.tar.bz2 crupest-03082a13dfeb92d3b3d605225937bea1ef2901fc.zip |
HALF WORK!:
Diffstat (limited to 'deno/mail-relay/mail-parsing.ts')
-rw-r--r-- | deno/mail-relay/mail-parsing.ts | 140 |
1 files changed, 140 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..8edaf77 --- /dev/null +++ b/deno/mail-relay/mail-parsing.ts @@ -0,0 +1,140 @@ +import emailAddresses from "email-addresses"; + +function parseHeaderSection(section: string) { + 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 Error(`Expect ':' in the header field line: ${field}`); + } + headers.push([field.slice(0, sepPos).trim(), field.slice(sepPos + 1)]); + field = null; + }; + + for (const line of section.trimEnd().split(/\r?\n|\r/)) { + if (line.match(/^\s/)) { + if (field == null) { + throw new Error("Header section starts with a space."); + } + field += line; + } else { + handleField(); + field = line; + } + lineNumber += 1; + } + + handleField(); + + return headers; +} + +function findFirst(fields: readonly [string, string][], key: string) { + for (const [k, v] of fields) { + if (key.toLowerCase() === k.toLowerCase()) return v; + } + return undefined; +} + +function findMessageId(fields: readonly [string, string][]) { + const messageIdField = findFirst(fields, "message-id"); + if (messageIdField == null) return undefined; + + const match = messageIdField.match(/\<(.*?)\>/); + if (match != null) { + return match[1]; + } else { + console.warn(`Invalid syntax in header 'message-id': ${messageIdField}`); + return undefined; + } +} + +function findDate(fields: readonly [string, string][]) { + const dateField = findFirst(fields, "date"); + if (dateField == null) return undefined; + + const date = new Date(dateField); + if (isNaN(date.getTime())) { + console.warn(`Invalid date string in header 'date': ${dateField}`); + return undefined; + } + return date; +} + +function findFrom(fields: readonly [string, string][]) { + const fromField = findFirst(fields, "from"); + if (fromField == null) return undefined; + + const addr = emailAddresses.parseOneAddress(fromField); + return addr?.type === "mailbox" ? addr.address : undefined; +} + +function findRecipients(fields: readonly [string, string][]) { + const headers = ["to", "cc", "bcc", "x-original-to"]; + const recipients = new Set<string>(); + for (const [key, value] of fields) { + if (headers.includes(key.toLowerCase())) { + emailAddresses + .parseAddressList(value) + ?.flatMap((a) => (a.type === "mailbox" ? a : a.addresses)) + ?.forEach(({ address }) => recipients.add(address)); + } + } + return recipients; +} + +function parseSections(raw: string) { + const twoEolMatch = raw.match(/(\r?\n)(\r?\n)/); + if (twoEolMatch == null) { + throw new Error( + "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: raw.slice(0, twoEolMatch.index!), + body: raw.slice(twoEolMatch.index! + eol.length + sep.length), + eol, + sep, + }; +} + +export type ParsedMail = Readonly<{ + header: string; + body: string; + sep: string; + eol: string; + headers: readonly [string, string][]; + messageId: string | undefined; + date: Date | undefined; + from: string | undefined; + recipients: readonly string[]; +}>; + +export function simpleParseMail(raw: string): ParsedMail { + const sections = Object.freeze(parseSections(raw)); + const headers = Object.freeze(parseHeaderSection(sections.header)); + const messageId = findMessageId(headers); + const date = findDate(headers); + const from = findFrom(headers); + const recipients = Object.freeze([...findRecipients(headers)]); + return Object.freeze({ + ...sections, + headers, + messageId, + date, + from, + recipients, + }); +} |