diff options
Diffstat (limited to 'services/docker/mail-server/aws-sendmail/mail.ts')
-rw-r--r-- | services/docker/mail-server/aws-sendmail/mail.ts | 104 |
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; } } |