aboutsummaryrefslogtreecommitdiff
path: root/services/docker/mail-server/aws-sendmail/mail.ts
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-04-30 00:20:23 +0800
committerYuqian Yang <crupest@crupest.life>2025-05-14 00:42:16 +0800
commit8ba08870fdb3bafa7b8739c4f1c57a70b8780143 (patch)
treea94c11d672aee89d5df907ad8027a4c8ea20f5b5 /services/docker/mail-server/aws-sendmail/mail.ts
parente69697300a5e238433faf2e3d1f6e54645a5a1bc (diff)
downloadcrupest-8ba08870fdb3bafa7b8739c4f1c57a70b8780143.tar.gz
crupest-8ba08870fdb3bafa7b8739c4f1c57a70b8780143.tar.bz2
crupest-8ba08870fdb3bafa7b8739c4f1c57a70b8780143.zip
HALF WORK!: 2025-5-14
Diffstat (limited to 'services/docker/mail-server/aws-sendmail/mail.ts')
-rw-r--r--services/docker/mail-server/aws-sendmail/mail.ts104
1 files changed, 82 insertions, 22 deletions
diff --git a/services/docker/mail-server/aws-sendmail/mail.ts b/services/docker/mail-server/aws-sendmail/mail.ts
index d4bcf75..98afcaa 100644
--- a/services/docker/mail-server/aws-sendmail/mail.ts
+++ b/services/docker/mail-server/aws-sendmail/mail.ts
@@ -1,5 +1,6 @@
import { encodeBase64 } from "@std/encoding/base64";
import { getLogger } from "./logger.ts";
+import { generateTimeStringForFileName } from "./util.ts";
export type MailDeliverStateNotSent = { kind: "not-sent" };
export type MailDeliverStateDelivered = {
@@ -21,8 +22,10 @@ class MailParseError extends Error {
constructor(
message: string,
public readonly mail: Mail,
+ public readonly lineNumber?: number,
options?: ErrorOptions,
) {
+ if (lineNumber != null) message += `(at line ${lineNumber})`;
super(message, options);
}
}
@@ -44,9 +47,14 @@ const eolNames = new Map([
]);
interface ParsedMail {
- headerStr: string;
- bodyStr: string;
- sepStr: string;
+ sections: {
+ header: string;
+ body: string;
+ };
+ /**
+ * The empty line between headers and body.
+ */
+ sep: string;
eol: string;
}
@@ -89,48 +97,100 @@ export class Mail {
// "\r\n" is a false positive.
if (twoEolMatch == null || twoEolMatch[0] === "\r\n") {
throw new MailParseError(
- "No header/body separator (2 successive EOLs) found. Cannot append headers.",
+ "No header/body section separator (2 successive EOLs) found.",
this,
);
}
- const [eol, sepStr] = [twoEolMatch[1], twoEolMatch[2]];
+ const [eol, sep] = [twoEolMatch[1], twoEolMatch[2]];
- if (eol !== sepStr) {
+ if (eol !== sep) {
getLogger().warn(
- `Different EOLs (${eolNames.get(eol)} \
- and ${eolNames.get(sepStr)}) found in mail.`,
+ `Different EOLs (${eolNames.get(eol)} and ${eolNames.get(sep)}) found.`,
);
}
return {
- headerStr: this.raw.slice(0, twoEolMatch.index! + eol.length),
- bodyStr: this.raw.slice(twoEolMatch.index! + eol.length + sepStr.length),
- sepStr,
+ sections: {
+ header: this.raw.slice(0, twoEolMatch.index! + eol.length),
+ body: this.raw.slice(twoEolMatch.index! + eol.length + sep.length),
+ },
+ sep,
eol,
};
}
simpleParseHeaders(): [key: string, value: string][] {
- const { headerStr } = this.simpleParse();
- const lines: string[] = [];
- for (const line of headerStr.split(/\r?\n|\r/)) {
+ const { sections } = this.simpleParse();
+ const headers: [string, string][] = [];
+
+ let field: string | null = null;
+ let lineNumber = 1;
+
+ const handleField = () => {
+ if (field == null) return;
+ const sepPos = field.indexOf(":");
+ if (sepPos === -1) {
+ throw new MailParseError(
+ "No ':' in the header field.",
+ this,
+ lineNumber,
+ );
+ }
+ headers.push([field.slice(0, sepPos).trim(), field.slice(sepPos + 1)]);
+ field = null;
+ };
+
+ for (const line of sections.header.trimEnd().split(/\r?\n|\r/)) {
if (line.match(/^\s/)) {
- if (lines.length === 0) {
- throw new MailParseError("Header part starts with a space.", this);
+ if (field == null) {
+ throw new MailParseError(
+ "Header field starts with a space.",
+ this,
+ lineNumber,
+ );
+ }
+ field += line;
+ } else {
+ handleField();
+ field = line;
+ }
+ lineNumber += 1;
+ }
+
+ handleField();
+
+ return headers;
+ }
+
+ simpleGetDate(): Date | null {
+ const headers = this.simpleParseHeaders();
+ for (const [key, value] of headers) {
+ if (key.toLowerCase() === "date") {
+ const date = new Date(value);
+ if (isNaN(date.getTime())) {
+ getLogger().warn(`Invalid date string (${value}) found in header.`);
+ return null;
}
- lines[lines.length - 1] += line;
+ return date;
}
}
+ return null;
+ }
- // TODO: Continue here.
+ simpleGetDateString<T>(fallback: T): string | T {
+ const date = this.simpleGetDate();
+ if (date == null) return fallback;
+ return generateTimeStringForFileName(date, true);
}
+ // TODO: Add folding.
appendHeaders(headers: [key: string, value: string][]) {
- const { headerStr, bodyStr, sepStr, eol } = this.simpleParse();
- const newHeaderStr = headerStr +
- headers.map(([k, v]) => `${k}: ${v}${eol}`).join("");
- this.raw = newHeaderStr + sepStr + bodyStr;
+ const { sections, sep, eol } = this.simpleParse();
+
+ this.raw = sections.header +
+ headers.map(([k, v]) => `${k}: ${v}`).join(eol) + eol + sep +
+ sections.body;
}
}