aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/retriver.ts31
-rw-r--r--services/docker/mail-server/aws-sendmail/mail.ts104
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;
}
}