aboutsummaryrefslogtreecommitdiff
path: root/deno/mail-relay/mail.ts
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-06-05 22:30:51 +0800
committerYuqian Yang <crupest@crupest.life>2025-06-09 21:48:00 +0800
commit3bdca0b90cf8bf5dfd6ff1ab482d857abb4acd2d (patch)
tree42fd1bf1f0119910c09542fbf475c012404658fd /deno/mail-relay/mail.ts
parent543fc733da074751e1750603df6931089efab465 (diff)
downloadcrupest-3bdca0b90cf8bf5dfd6ff1ab482d857abb4acd2d.tar.gz
crupest-3bdca0b90cf8bf5dfd6ff1ab482d857abb4acd2d.tar.bz2
crupest-3bdca0b90cf8bf5dfd6ff1ab482d857abb4acd2d.zip
feat(deno): move deno (mail-server) to top level.
Diffstat (limited to 'deno/mail-relay/mail.ts')
-rw-r--r--deno/mail-relay/mail.ts133
1 files changed, 65 insertions, 68 deletions
diff --git a/deno/mail-relay/mail.ts b/deno/mail-relay/mail.ts
index af0df40..8c2e067 100644
--- a/deno/mail-relay/mail.ts
+++ b/deno/mail-relay/mail.ts
@@ -2,24 +2,19 @@ import { encodeBase64 } from "@std/encoding/base64";
import { parse } from "@std/csv/parse";
import emailAddresses from "email-addresses";
-import log from "./log.ts";
-import config from "./config.ts";
+import { Logger } from "@crupest/base/log";
-class MailSimpleParseError extends Error {
- constructor(
- message: string,
- public readonly text: string,
- public readonly lineNumber?: number,
- options?: ErrorOptions,
- ) {
- if (lineNumber != null) message += `(at line ${lineNumber})`;
- super(message, options);
+class MailSimpleParseError extends Error { }
+
+class MailSimpleParsedHeaders {
+ #logger
+
+ constructor(logger: Logger | undefined, public fields: [key:string, value: string][]) {
+ this.#logger = logger;
}
-}
-class MailSimpleParsedHeaders extends Array<[key: string, value: string]> {
getFirst(fieldKey: string): string | undefined {
- for (const [key, value] of this) {
+ for (const [key, value] of this.fields) {
if (key.toLowerCase() === fieldKey.toLowerCase()) return value;
}
return undefined;
@@ -33,7 +28,7 @@ class MailSimpleParsedHeaders extends Array<[key: string, value: string]> {
if (match != null) {
return match[1];
} else {
- console.warn("Invalid message-id header of mail: ", messageIdField);
+ this.#logger?.warn("Invalid message-id header of mail: ", messageIdField);
return undefined;
}
}
@@ -44,7 +39,7 @@ class MailSimpleParsedHeaders extends Array<[key: string, value: string]> {
const date = new Date(dateField);
if (invalidToUndefined && isNaN(date.getTime())) {
- log.warn(`Invalid date string (${dateField}) found in header.`);
+ this.#logger?.warn(`Invalid date string (${dateField}) found in header.`);
return undefined;
}
return date;
@@ -54,15 +49,16 @@ class MailSimpleParsedHeaders extends Array<[key: string, value: 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) {
+ 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);
- }
- });
+ emailAddresses
+ .parseAddressList(value)
+ ?.flatMap((a) => (a.type === "mailbox" ? a : a.addresses))
+ ?.forEach(({ address }) => {
+ if (domain == null || address.endsWith(domain)) {
+ recipients.add(address);
+ }
+ });
}
}
return recipients;
@@ -75,19 +71,22 @@ class MailSimpleParsedSections {
eol: string;
sep: string;
- constructor(raw: string) {
+ #logger
+
+ constructor(logger: Logger | undefined, raw: string) {
+ this.#logger = logger
+
const twoEolMatch = raw.match(/(\r?\n)(\r?\n)/);
if (twoEolMatch == null) {
throw new MailSimpleParseError(
"No header/body section separator (2 successive EOLs) found.",
- raw,
);
}
const [eol, sep] = [twoEolMatch[1], twoEolMatch[2]];
if (eol !== sep) {
- log.warn("Different EOLs (\\r\\n, \\n) found.");
+ logger?.warn("Different EOLs (\\r\\n, \\n) found.");
}
this.header = raw.slice(0, twoEolMatch.index!);
@@ -97,7 +96,7 @@ class MailSimpleParsedSections {
}
headers(): MailSimpleParsedHeaders {
- const headers = new MailSimpleParsedHeaders();
+ const headers = [] as [key:string, value: string][];
let field: string | null = null;
let lineNumber = 1;
@@ -107,9 +106,7 @@ class MailSimpleParsedSections {
const sepPos = field.indexOf(":");
if (sepPos === -1) {
throw new MailSimpleParseError(
- "No ':' in the header field.",
- this.header,
- lineNumber,
+ `No ':' in the header line: ${field}`,
);
}
headers.push([field.slice(0, sepPos).trim(), field.slice(sepPos + 1)]);
@@ -119,11 +116,7 @@ class MailSimpleParsedSections {
for (const line of this.header.trimEnd().split(/\r?\n|\r/)) {
if (line.match(/^\s/)) {
if (field == null) {
- throw new MailSimpleParseError(
- "Header field starts with a space.",
- this.header,
- lineNumber,
- );
+ throw new MailSimpleParseError("Header section starts with a space.");
}
field += line;
} else {
@@ -135,7 +128,7 @@ class MailSimpleParsedSections {
handleField();
- return headers;
+ return new MailSimpleParsedHeaders(this.#logger, headers);
}
}
@@ -151,23 +144,14 @@ export class Mail {
return encodeBase64(this.raw);
}
- startSimpleParse() {
- return { sections: () => new MailSimpleParsedSections(this.raw) };
+ startSimpleParse(logger?: Logger) {
+ return { sections: () => new MailSimpleParsedSections(logger, this.raw) };
}
simpleFindAllAddresses(): string[] {
const re = /,?\<?([a-z0-9_'+\-\.]+\@[a-z0-9_'+\-\.]+)\>?,?/ig
return [...this.raw.matchAll(re)].map(m => m[1])
}
-
- // TODO: Add folding.
- appendHeaders(headers: [key: string, value: string][]) {
- const { header, body, sep, eol } = this.startSimpleParse().sections();
-
- this.raw = header + eol +
- headers.map(([k, v]) => `${k}: ${v}`).join(eol) + eol + sep +
- body;
- }
}
export type MailDeliverResultKind = "done" | "fail";
@@ -203,7 +187,10 @@ export class MailDeliverContext {
readonly recipients: Set<string> = new Set();
readonly result;
- constructor(public mail: Mail) {
+ constructor(
+ public readonly logger: Logger,
+ public mail: Mail,
+ ) {
this.result = new MailDeliverResult(this.mail);
}
}
@@ -217,6 +204,8 @@ export abstract class MailDeliverer {
preHooks: MailDeliverHook[] = [];
postHooks: MailDeliverHook[] = [];
+ constructor(protected readonly logger: Logger) { }
+
protected abstract doDeliver(
mail: Mail,
context: MailDeliverContext,
@@ -226,12 +215,13 @@ export abstract class MailDeliverer {
return await this.deliver({ mail: new Mail(rawMail) });
}
- async deliver(
- options: { mail: Mail; recipients?: string[] },
- ): Promise<MailDeliverResult> {
- log.info(`Begin to deliver mail via ${this.name}...`);
+ async deliver(options: {
+ mail: Mail;
+ recipients?: string[];
+ }): Promise<MailDeliverResult> {
+ this.logger.info(`Begin to deliver mail via ${this.name}...`);
- const context = new MailDeliverContext(options.mail);
+ const context = new MailDeliverContext(this.logger, options.mail);
options.recipients?.forEach((r) => context.recipients.add(r));
for (const hook of this.preHooks) {
@@ -244,8 +234,8 @@ export abstract class MailDeliverer {
await hook.callback(context);
}
- log.info("Deliver result:");
- log.info(context.result);
+ context.logger.info("Deliver result:");
+ context.logger.info(context.result);
if (context.result.hasError()) {
throw new Error("Mail failed to deliver.");
@@ -261,7 +251,7 @@ export abstract class SyncMailDeliverer extends MailDeliverer {
override async deliver(
options: { mail: Mail; recipients?: string[] },
): Promise<MailDeliverResult> {
- log.info("The mail deliverer is sync. Wait for last delivering done...");
+ this.logger.info("The mail deliverer is sync. Wait for last delivering done...");
await this.#last;
const result = super.deliver(options);
this.#last = result.then(() => {}, () => {});
@@ -270,17 +260,24 @@ export abstract class SyncMailDeliverer extends MailDeliverer {
}
export class RecipientFromHeadersHook implements MailDeliverHook {
+ constructor(public mailDomain: string) {}
+
callback(context: MailDeliverContext) {
if (context.recipients.size !== 0) {
- log.warn(
+ context.logger.warn(
"Recipients are already filled. Won't set them with ones in headers.",
);
} else {
- context.mail.startSimpleParse().sections().headers().recipients({
- domain: config.get("mailDomain"),
- }).forEach((r) => context.recipients.add(r));
-
- log.info(
+ context.mail
+ .startSimpleParse(context.logger)
+ .sections()
+ .headers()
+ .recipients({
+ domain: this.mailDomain,
+ })
+ .forEach((r) => context.recipients.add(r));
+
+ context.logger.info(
"Recipients found from mail headers: ",
[...context.recipients].join(" "),
);
@@ -294,7 +291,7 @@ export class FallbackRecipientHook implements MailDeliverHook {
callback(context: MailDeliverContext) {
if (context.recipients.size === 0) {
- log.info(
+ context.logger.info(
"No recipients, fill with fallback: ",
[...this.fallback].join(" "),
);
@@ -311,10 +308,10 @@ export class AliasRecipientMailHook implements MailDeliverHook {
this.#aliasFile = aliasFile;
}
- async #parseAliasFile(): Promise<Map<string, string>> {
+ async #parseAliasFile(logger: Logger): Promise<Map<string, string>> {
const result = new Map();
if ((await Deno.stat(this.#aliasFile)).isFile) {
- log.info(`Found recipients alias file: ${this.#aliasFile}.`);
+ logger.info(`Found recipients alias file: ${this.#aliasFile}.`);
const text = await Deno.readTextFile(this.#aliasFile);
const csv = parse(text);
for (const [real, ...aliases] of csv) {
@@ -325,11 +322,11 @@ export class AliasRecipientMailHook implements MailDeliverHook {
}
async callback(context: MailDeliverContext) {
- const aliases = await this.#parseAliasFile();
+ const aliases = await this.#parseAliasFile(context.logger);
for (const recipient of [...context.recipients]) {
const realRecipients = aliases.get(recipient);
if (realRecipients != null) {
- log.info(
+ context.logger.info(
`Recipient alias resolved: ${recipient} => ${realRecipients}.`,
);
context.recipients.delete(recipient);