aboutsummaryrefslogtreecommitdiff
path: root/deno/mail-relay/mail-parsing.ts
diff options
context:
space:
mode:
Diffstat (limited to 'deno/mail-relay/mail-parsing.ts')
-rw-r--r--deno/mail-relay/mail-parsing.ts196
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);
+}