diff options
| author | Yuqian Yang <crupest@crupest.life> | 2025-04-30 00:20:23 +0800 | 
|---|---|---|
| committer | Yuqian Yang <crupest@crupest.life> | 2025-05-14 00:42:16 +0800 | 
| commit | 8ba08870fdb3bafa7b8739c4f1c57a70b8780143 (patch) | |
| tree | a94c11d672aee89d5df907ad8027a4c8ea20f5b5 /services/docker/mail-server/aws-sendmail | |
| parent | e69697300a5e238433faf2e3d1f6e54645a5a1bc (diff) | |
| download | crupest-8ba08870fdb3bafa7b8739c4f1c57a70b8780143.tar.gz crupest-8ba08870fdb3bafa7b8739c4f1c57a70b8780143.tar.bz2 crupest-8ba08870fdb3bafa7b8739c4f1c57a70b8780143.zip | |
HALF WORK!: 2025-5-14
Diffstat (limited to 'services/docker/mail-server/aws-sendmail')
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/aws/retriver.ts | 31 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/mail.ts | 104 | 
2 files changed, 101 insertions, 34 deletions
| diff --git a/services/docker/mail-server/aws-sendmail/aws/retriver.ts b/services/docker/mail-server/aws-sendmail/aws/retriver.ts index 1544c04..b599c15 100644 --- a/services/docker/mail-server/aws-sendmail/aws/retriver.ts +++ b/services/docker/mail-server/aws-sendmail/aws/retriver.ts @@ -6,10 +6,10 @@ import {    S3Client,  } from "@aws-sdk/client-s3"; -import { generateTimeStringForFileName } from "../util.ts"; -import { getLogger } from "../logger.ts";  import { AwsContext, s3MoveObject } from "./context.ts"; +import { getLogger } from "../logger.ts";  import { getConfig } from "../config.ts"; +import { Mail } from "../mail.ts";  export class AwsMailRetriever {    readonly liveMailPrefix = "mail/live/"; @@ -21,7 +21,7 @@ export class AwsMailRetriever {    constructor(      aws: AwsContext, -    private readonly callback: (rawMail: string) => Promise<void>, +    private readonly callback: (mail: Mail) => Promise<void>,    ) {      const { region, credentials } = aws;      this.s3Client = new S3Client({ region, credentials }); @@ -34,12 +34,6 @@ export class AwsMailRetriever {      });    } -  generateArchivePrefix(instant: Date | Temporal.Instant): string { -    return `${this.archiveMailPrefix}${ -      generateTimeStringForFileName(instant, true) -    }/`; -  } -    async listLiveMails(): Promise<string[]> {      const listCommand = new ListObjectsV2Command({        Bucket: this.mailBucket, @@ -55,7 +49,8 @@ export class AwsMailRetriever {      const result: string[] = [];      for (const object of res.Contents) {        if (object.Key != null) { -        result.push(object.Key); +        // TODO: check prefix consistence here. +        result.push(object.Key.slice(this.liveMailPrefix.length));        } else {          getLogger().warn(            "Listing live mails in S3 returns an object with no Key.", @@ -79,13 +74,25 @@ export class AwsMailRetriever {      }      const rawMail = await res.Body.transformToString(); -    await this.callback(rawMail); +    const mail = new Mail(rawMail); +    mail.awsMessageId = messageId; +    await this.callback(mail);      // TODO: Continue here. -    await s3MoveObject(this.s3Client, this.mailBucket, mailPath, ); +    await s3MoveObject( +      this.s3Client, +      this.mailBucket, +      mailPath, +      `${this.archiveMailPrefix}${ +        mail.simpleGetDateString("invalid-date") +      }/${messageId}`, +    );    }    async recycleLiveMails() {      const mails = await this.listLiveMails(); +    for (const messageId of mails) { +      await this.deliverS3MailObject(messageId); +    }    }  } 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;    }  } | 
