aboutsummaryrefslogtreecommitdiff
path: root/deno/mail-relay/mail.ts
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-06-17 19:04:16 +0800
committerYuqian Yang <crupest@crupest.life>2025-06-17 21:37:43 +0800
commit874c4a8babc5aac7214e71dfef7743bae23893a3 (patch)
treed7d39313a442bac49859669bb8d1919267575023 /deno/mail-relay/mail.ts
parent0824de3bae3550674a9ea029b03c5cb8a35cd8e1 (diff)
downloadcrupest-874c4a8babc5aac7214e71dfef7743bae23893a3.tar.gz
crupest-874c4a8babc5aac7214e71dfef7743bae23893a3.tar.bz2
crupest-874c4a8babc5aac7214e71dfef7743bae23893a3.zip
HALF WORK!:mail
Diffstat (limited to 'deno/mail-relay/mail.ts')
-rw-r--r--deno/mail-relay/mail.ts202
1 files changed, 31 insertions, 171 deletions
diff --git a/deno/mail-relay/mail.ts b/deno/mail-relay/mail.ts
index f1fc892..f4e3e6d 100644
--- a/deno/mail-relay/mail.ts
+++ b/deno/mail-relay/mail.ts
@@ -1,135 +1,28 @@
import { encodeBase64 } from "@std/encoding/base64";
import { parse } from "@std/csv/parse";
-import emailAddresses from "email-addresses";
+import { simpleParseMail } from "./mail-parsing.ts";
-class MailSimpleParseError extends Error {}
-
-class MailSimpleParsedHeaders {
- constructor(public fields: [key: string, value: string][]) {}
-
- getFirst(fieldKey: string): string | undefined {
- for (const [key, value] of this.fields) {
- if (key.toLowerCase() === fieldKey.toLowerCase()) return value;
- }
- return undefined;
- }
-
- messageId(): string | undefined {
- const messageIdField = this.getFirst("message-id");
- if (messageIdField == null) return undefined;
+export class Mail {
+ #raw;
+ #parsed;
- const match = messageIdField.match(/\<(.*?)\>/);
- if (match != null) {
- return match[1];
- } else {
- console.warn("Invalid message-id header of mail: " + messageIdField);
- return undefined;
- }
+ constructor(raw: string) {
+ this.#raw = raw;
+ this.#parsed = simpleParseMail(raw);
}
- date(invalidToUndefined: boolean = true): Date | undefined {
- const dateField = this.getFirst("date");
- if (dateField == null) return undefined;
-
- const date = new Date(dateField);
- if (invalidToUndefined && isNaN(date.getTime())) {
- console.warn(`Invalid date string (${dateField}) found in header.`);
- return undefined;
- }
- return date;
+ get raw() {
+ return this.#raw;
}
- from(): string | undefined {
- const fromField = this.getFirst("from");
- if (fromField == null) return undefined;
-
- const addr = emailAddresses.parseOneAddress(fromField);
- return addr?.type === "mailbox" ? addr.address : undefined;
+ set raw(value) {
+ this.#raw = value;
+ this.#parsed = simpleParseMail(value);
}
- recipients(options?: { domain?: string; headers?: string[] }): Set<string> {
- const domain = options?.domain;
- const headers = options?.headers ?? ["to", "cc", "bcc", "x-original-to"];
- const recipients = new Set<string>();
- for (const [key, value] of this.fields) {
- if (headers.includes(key.toLowerCase())) {
- emailAddresses
- .parseAddressList(value)
- ?.flatMap((a) => (a.type === "mailbox" ? a : a.addresses))
- ?.forEach(({ address }) => {
- if (domain == null || address.endsWith(domain)) {
- recipients.add(address);
- }
- });
- }
- }
- return recipients;
+ get parsed() {
+ return this.#parsed;
}
-}
-
-class MailSimpleParsedSections {
- header: string;
- body: string;
- eol: string;
- sep: string;
-
- constructor(raw: string) {
- const twoEolMatch = 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.");
- }
-
- this.header = raw.slice(0, twoEolMatch.index!);
- this.body = raw.slice(twoEolMatch.index! + eol.length + sep.length);
- this.eol = eol;
- this.sep = sep;
- }
-
- headers(): MailSimpleParsedHeaders {
- 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.header.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 new MailSimpleParsedHeaders(headers);
- }
-}
-
-export class Mail {
- constructor(public raw: string) {}
toUtf8Bytes(): Uint8Array {
const utf8Encoder = new TextEncoder();
@@ -140,44 +33,23 @@ export class Mail {
return encodeBase64(this.raw);
}
- startSimpleParse() {
- return { sections: () => new MailSimpleParsedSections(this.raw) };
- }
-
simpleFindAllAddresses(): string[] {
const re = /,?\<?([a-z0-9_'+\-\.]+\@[a-z0-9_'+\-\.]+)\>?,?/gi;
return [...this.raw.matchAll(re)].map((m) => m[1]);
}
}
-export type MailDeliverResultKind = "done" | "fail";
-
export interface MailDeliverRecipientResult {
- kind: MailDeliverResultKind;
- message: string;
+ kind: "success" | "failure";
+ message?: string;
cause?: unknown;
}
export class MailDeliverResult {
- smtpMessage: string = "";
- recipients: Map<string, MailDeliverRecipientResult> = new Map();
-
+ message?: string;
+ smtpMessage?: string;
+ recipients = new Map<string, MailDeliverRecipientResult>();
constructor(public mail: Mail) {}
-
- hasError(): boolean {
- return (
- this.recipients.size === 0 ||
- this.recipients.values().some((r) => r.kind !== "done")
- );
- }
-
- [Symbol.for("Deno.customInspect")]() {
- return [
- ...this.recipients.entries().map(([recipient, result]) =>
- `${recipient} [${result.kind}]: ${result.message}`
- ),
- ].join("\n");
- }
}
export class MailDeliverContext {
@@ -194,7 +66,6 @@ export interface MailDeliverHook {
}
export abstract class MailDeliverer {
- abstract readonly name: string;
preHooks: MailDeliverHook[] = [];
postHooks: MailDeliverHook[] = [];
@@ -211,11 +82,11 @@ export abstract class MailDeliverer {
mail: Mail;
recipients?: string[];
}): Promise<MailDeliverResult> {
- console.info(`Begin to deliver mail via ${this.name}...`);
-
const context = new MailDeliverContext(options.mail);
options.recipients?.forEach((r) => context.recipients.add(r));
+ console.info("Begin to deliver mail to...");
+
for (const hook of this.preHooks) {
await hook.callback(context);
}
@@ -226,12 +97,7 @@ export abstract class MailDeliverer {
await hook.callback(context);
}
- console.info("Deliver result:");
- console.info(context.result);
-
- if (context.result.hasError()) {
- throw new Error("Mail failed to deliver.");
- }
+ console.info("Deliver result:", context.result);
return context.result;
}
@@ -263,21 +129,16 @@ export class RecipientFromHeadersHook implements MailDeliverHook {
callback(context: MailDeliverContext) {
if (context.recipients.size !== 0) {
console.warn(
- "Recipients are already filled. Won't set them with ones in headers.",
+ "Recipients are already filled, skip inferring from headers.",
);
} else {
- context.mail
- .startSimpleParse()
- .sections()
- .headers()
- .recipients({
- domain: this.mailDomain,
- })
- .forEach((r) => context.recipients.add(r));
+ [...context.mail.parsed.recipients].filter((r) =>
+ r.endsWith("@" + this.mailDomain)
+ ).forEach((r) => context.recipients.add(r));
console.info(
- "Recipients found from mail headers: " +
- [...context.recipients].join(", "),
+ "Use recipients inferred from mail headers:",
+ [...context.recipients].join(", "),
);
}
return Promise.resolve();
@@ -289,9 +150,7 @@ export class FallbackRecipientHook implements MailDeliverHook {
callback(context: MailDeliverContext) {
if (context.recipients.size === 0) {
- console.info(
- "No recipients, fill with fallback: " + [...this.fallback].join(", "),
- );
+ console.info("Use fallback recipients:" + [...this.fallback].join(", "));
this.fallback.forEach((a) => context.recipients.add(a));
}
return Promise.resolve();
@@ -308,12 +167,13 @@ export class AliasRecipientMailHook implements MailDeliverHook {
async #parseAliasFile(): Promise<Map<string, string>> {
const result = new Map();
if ((await Deno.stat(this.#aliasFile)).isFile) {
- console.info(`Found recipients alias file: ${this.#aliasFile}.`);
const text = await Deno.readTextFile(this.#aliasFile);
const csv = parse(text);
for (const [real, ...aliases] of csv) {
aliases.forEach((a) => result.set(a, real));
}
+ } else {
+ console.warn(`Recipient alias file ${this.#aliasFile} is not found.`);
}
return result;
}