aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/base.ts15
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/context.ts41
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/deliver.ts2
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/retriver.ts32
-rw-r--r--services/docker/mail-server/aws-sendmail/base.ts34
-rw-r--r--services/docker/mail-server/aws-sendmail/config.ts56
-rw-r--r--services/docker/mail-server/aws-sendmail/db.ts9
-rw-r--r--services/docker/mail-server/aws-sendmail/logger.ts16
-rw-r--r--services/docker/mail-server/aws-sendmail/mail.ts14
-rw-r--r--services/docker/mail-server/aws-sendmail/main.ts4
-rw-r--r--services/docker/mail-server/aws-sendmail/traffic.ts2
-rw-r--r--services/docker/mail-server/aws-sendmail/util.ts49
12 files changed, 188 insertions, 86 deletions
diff --git a/services/docker/mail-server/aws-sendmail/aws/base.ts b/services/docker/mail-server/aws-sendmail/aws/base.ts
deleted file mode 100644
index 1e23009..0000000
--- a/services/docker/mail-server/aws-sendmail/aws/base.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { getEnvRequired } from "../base.ts";
-
-export class AwsContext {
- readonly region = "ap-southeast-1";
-
- accessKeyId = getEnvRequired("AWS_USER", "aws access key id");
- secretAccessKey = getEnvRequired("AWS_PASSWORD", "aws secret access key");
-
- getCredentials() {
- const { accessKeyId, secretAccessKey } = this;
- return Promise.resolve({ accessKeyId, secretAccessKey });
- }
-
- readonly credentials = this.getCredentials.bind(this);
-}
diff --git a/services/docker/mail-server/aws-sendmail/aws/context.ts b/services/docker/mail-server/aws-sendmail/aws/context.ts
new file mode 100644
index 0000000..65c8371
--- /dev/null
+++ b/services/docker/mail-server/aws-sendmail/aws/context.ts
@@ -0,0 +1,41 @@
+import {
+ CopyObjectCommand,
+ DeleteObjectCommand,
+ S3Client,
+} from "@aws-sdk/client-s3";
+
+import { getConfig } from "../config.ts";
+
+export class AwsContext {
+ readonly region = "ap-southeast-1";
+
+ accessKeyId = getConfig().getValue("awsAccessKeyId");
+ secretAccessKey = getConfig().getValue("awsSecretAccessKey");
+
+ getCredentials() {
+ const { accessKeyId, secretAccessKey } = this;
+ return Promise.resolve({ accessKeyId, secretAccessKey });
+ }
+
+ readonly credentials = this.getCredentials.bind(this);
+}
+
+export async function s3MoveObject(
+ client: S3Client,
+ bucket: string,
+ path: string,
+ newPath: string,
+): Promise<void> {
+ const copyCommand = new CopyObjectCommand({
+ Bucket: bucket,
+ Key: newPath,
+ CopySource: path,
+ });
+ await client.send(copyCommand);
+
+ const deleteCommand = new DeleteObjectCommand({
+ Bucket: bucket,
+ Key: path,
+ });
+ await client.send(deleteCommand);
+}
diff --git a/services/docker/mail-server/aws-sendmail/aws/deliver.ts b/services/docker/mail-server/aws-sendmail/aws/deliver.ts
index da100f5..5982d56 100644
--- a/services/docker/mail-server/aws-sendmail/aws/deliver.ts
+++ b/services/docker/mail-server/aws-sendmail/aws/deliver.ts
@@ -1,6 +1,6 @@
import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2";
-import { AwsContext } from "./base.ts";
+import { AwsContext } from "./context.ts";
import { Mail, MailDeliverer } from "../mail.ts";
export class AwsMailDeliverer extends MailDeliverer {
diff --git a/services/docker/mail-server/aws-sendmail/aws/retriver.ts b/services/docker/mail-server/aws-sendmail/aws/retriver.ts
index cdfe6f1..1544c04 100644
--- a/services/docker/mail-server/aws-sendmail/aws/retriver.ts
+++ b/services/docker/mail-server/aws-sendmail/aws/retriver.ts
@@ -6,23 +6,23 @@ import {
S3Client,
} from "@aws-sdk/client-s3";
-import { generateTimeStringForFileName, getEnvRequired } from "../base.ts";
+import { generateTimeStringForFileName } from "../util.ts";
import { getLogger } from "../logger.ts";
-import { AwsContext } from "./base.ts";
-import { MailDeliverer } from "../mail.ts";
+import { AwsContext, s3MoveObject } from "./context.ts";
+import { getConfig } from "../config.ts";
export class AwsMailRetriever {
- mailBucket = getEnvRequired(
- "AWS_MAIL_BUCKET",
- "aws s3 bucket saving raw mails",
- );
- liveMailPrefix = "mail/live/";
- archiveMailPrefix = "mail/archive/";
+ readonly liveMailPrefix = "mail/live/";
+ readonly archiveMailPrefix = "mail/archive/";
+ readonly mailBucket = getConfig().getValue("awsMailBucket");
- private s3Client;
- private liveMailRecyclerAborter = new AbortController();
+ private readonly s3Client;
+ private readonly liveMailRecyclerAborter = new AbortController();
- constructor(private aws: AwsContext, private localDeliverer: MailDeliverer) {
+ constructor(
+ aws: AwsContext,
+ private readonly callback: (rawMail: string) => Promise<void>,
+ ) {
const { region, credentials } = aws;
this.s3Client = new S3Client({ region, credentials });
}
@@ -66,9 +66,10 @@ export class AwsMailRetriever {
}
async deliverS3MailObject(messageId: string) {
+ const mailPath = `${this.liveMailPrefix}${messageId}`;
const command = new GetObjectCommand({
Bucket: this.mailBucket,
- Key: `${this.liveMailPrefix}${messageId}`,
+ Key: mailPath,
});
const res = await this.s3Client.send(command);
@@ -78,9 +79,10 @@ export class AwsMailRetriever {
}
const rawMail = await res.Body.transformToString();
- await this.localDeliverer.deliverRaw(rawMail);
+ await this.callback(rawMail);
- const archiveCommand = new
+ // TODO: Continue here.
+ await s3MoveObject(this.s3Client, this.mailBucket, mailPath, );
}
async recycleLiveMails() {
diff --git a/services/docker/mail-server/aws-sendmail/base.ts b/services/docker/mail-server/aws-sendmail/base.ts
deleted file mode 100644
index 08b592a..0000000
--- a/services/docker/mail-server/aws-sendmail/base.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-export const APP_PREFIX = "crupest";
-export const APP_NAME = "mailserver";
-
-export function getEnvRequired(key: string, usage: string): string {
- key = `${APP_PREFIX.toUpperCase()}_${APP_NAME.toUpperCase()}_${key}`;
- const value = Deno.env.get(key);
- if (value == null) {
- throw new Error(`Env ${key} does not exist, used for ${usage}.`);
- }
- return value;
-}
-
-function getZonedDateTime(instant?: Temporal.Instant | Date) {
- if (instant == null) {
- instant = Temporal.Now.instant();
- } else if (instant instanceof Date) {
- instant = instant.toTemporalInstant();
- }
-
- return instant.toZonedDateTimeISO("UTC");
-}
-
-export function generateTimeStringForFileName(
- instant?: Temporal.Instant | Date,
- dateOnly: boolean = false,
-): string {
- const time = getZonedDateTime(instant);
-
- if (dateOnly) {
- return time.toPlainDate().toString();
- } else {
- return time.toPlainDateTime().toString().replaceAll(/:|\./g, "-");
- }
-}
diff --git a/services/docker/mail-server/aws-sendmail/config.ts b/services/docker/mail-server/aws-sendmail/config.ts
new file mode 100644
index 0000000..0212029
--- /dev/null
+++ b/services/docker/mail-server/aws-sendmail/config.ts
@@ -0,0 +1,56 @@
+import { createSingleton, transformProperties } from "./util.ts";
+
+export const APP_PREFIX = "crupest";
+export const APP_NAME = "mailserver";
+
+interface ConfigItemDef {
+ env: string;
+ description: string;
+ default?: string;
+}
+
+export const CONFIG_DEFS = {
+ awsAccessKeyId: { env: "AWS_USER", description: "aws access key id" },
+ awsSecretAccessKey: {
+ env: "AWS_PASSWORD",
+ description: "aws secret access key",
+ },
+ awsMailBucket: {
+ env: "AWS_MAIL_BUCKET",
+ description: "aws s3 bucket saving raw mails",
+ },
+} as const satisfies Record<string, ConfigItemDef>;
+
+type ConfigDefs = typeof CONFIG_DEFS;
+type ConfigKey = keyof ConfigDefs;
+type ConfigMap = {
+ [K in ConfigKey]: ConfigDefs[K] & { value: string };
+};
+
+function resolveAppConfigItem(def: ConfigItemDef): string {
+ const envKey =
+ `${APP_PREFIX.toUpperCase()}_${APP_NAME.toUpperCase()}_${def.env}`;
+ const value = Deno.env.get(envKey);
+ if (value != null) return value;
+ if (def.default != null) return def.default;
+ throw new Error(
+ `Required env ${envKey} (${def.description}) is not set.`,
+ );
+}
+
+export class Config {
+ private config = transformProperties(
+ CONFIG_DEFS,
+ (def) => ({ ...def, value: resolveAppConfigItem(def) }),
+ ) as ConfigMap;
+
+ get<K extends ConfigKey>(key: K): ConfigMap[K] {
+ return this.config[key];
+ }
+
+ getValue(key: ConfigKey): string {
+ return this.get(key).value;
+ }
+}
+
+export const [getConfig, setConfig] = createSingleton<Config>("Config");
diff --git a/services/docker/mail-server/aws-sendmail/db.ts b/services/docker/mail-server/aws-sendmail/db.ts
index 6a7b100..e239e72 100644
--- a/services/docker/mail-server/aws-sendmail/db.ts
+++ b/services/docker/mail-server/aws-sendmail/db.ts
@@ -72,7 +72,14 @@ export class DbService {
private _migrator;
constructor(public readonly path: string) {
- this._sqlocal = new SQLocalKysely("database.sqlite3");
+ this._sqlocal = new SQLocalKysely({
+ databasePath: path,
+ onInit: (sql) => [
+ // Though this can be executed only once when database is
+ // created, re-calls should not affect performance.
+ sql`PRAGMA journal_mode=WAL;`,
+ ],
+ });
const db = new Kysely<Database>({ dialect: this._sqlocal.dialect });
this._db = db;
this._migrator = new Migrator({
diff --git a/services/docker/mail-server/aws-sendmail/logger.ts b/services/docker/mail-server/aws-sendmail/logger.ts
index 3a0cff6..160d70a 100644
--- a/services/docker/mail-server/aws-sendmail/logger.ts
+++ b/services/docker/mail-server/aws-sendmail/logger.ts
@@ -1,7 +1,6 @@
import * as path from "@std/path";
-import { generateTimeStringForFileName } from "./base.ts";
-
+import { createSingleton, generateTimeStringForFileName } from "./util.ts";
export class Logger {
constructor(public readonly path: string) {}
@@ -48,15 +47,4 @@ export class Logger {
}
}
-let _logger: Logger | null = null;
-
-export function getLogger(): Logger {
- if (_logger == null) {
- throw new Error("No logger is set now.");
- }
- return _logger;
-}
-
-export function setLogger(logger: Logger | null) {
- _logger = logger;
-}
+export const [getLogger, setLogger] = createSingleton<Logger>("Logger");
diff --git a/services/docker/mail-server/aws-sendmail/mail.ts b/services/docker/mail-server/aws-sendmail/mail.ts
index 82902d2..d4bcf75 100644
--- a/services/docker/mail-server/aws-sendmail/mail.ts
+++ b/services/docker/mail-server/aws-sendmail/mail.ts
@@ -113,11 +113,17 @@ export class Mail {
simpleParseHeaders(): [key: string, value: string][] {
const { headerStr } = this.simpleParse();
- const rawLines = headerStr.split(/\r\n|\n|\r/);
- const headerLines = [];
- for (const l of rawLines) {
-
+ const lines: string[] = [];
+ for (const line of headerStr.split(/\r?\n|\r/)) {
+ if (line.match(/^\s/)) {
+ if (lines.length === 0) {
+ throw new MailParseError("Header part starts with a space.", this);
+ }
+ lines[lines.length - 1] += line;
+ }
}
+
+ // TODO: Continue here.
}
appendHeaders(headers: [key: string, value: string][]) {
diff --git a/services/docker/mail-server/aws-sendmail/main.ts b/services/docker/mail-server/aws-sendmail/main.ts
index 7207188..dcc2af1 100644
--- a/services/docker/mail-server/aws-sendmail/main.ts
+++ b/services/docker/mail-server/aws-sendmail/main.ts
@@ -1,6 +1,7 @@
-import { AwsContext } from "./aws/base.ts";
+import { AwsContext } from "./aws/context.ts";
import { MailTrafficHandler } from "./traffic.ts";
import { Logger, setLogger } from "./logger.ts";
+import { Config, setConfig } from "./config.ts";
class App {
readonly aws = new AwsContext();
@@ -8,6 +9,7 @@ class App {
constructor() {
setLogger(new Logger("log"));
+ setConfig(new Config());
}
}
diff --git a/services/docker/mail-server/aws-sendmail/traffic.ts b/services/docker/mail-server/aws-sendmail/traffic.ts
index 4e92557..7ecc405 100644
--- a/services/docker/mail-server/aws-sendmail/traffic.ts
+++ b/services/docker/mail-server/aws-sendmail/traffic.ts
@@ -1,7 +1,7 @@
import { MailDeliverer } from "./mail.ts";
import { DbService } from "./db.ts";
import { DovecotMailDeliverer } from "./dovecot.ts";
-import { AwsContext } from "./aws/base.ts";
+import { AwsContext } from "./aws/context.ts";
import { AwsMailDeliverer } from "./aws/deliver.ts";
export abstract class MailTrafficHandler {
diff --git a/services/docker/mail-server/aws-sendmail/util.ts b/services/docker/mail-server/aws-sendmail/util.ts
new file mode 100644
index 0000000..1411840
--- /dev/null
+++ b/services/docker/mail-server/aws-sendmail/util.ts
@@ -0,0 +1,49 @@
+function getZonedDateTime(instant?: Temporal.Instant | Date) {
+ if (instant == null) {
+ instant = Temporal.Now.instant();
+ } else if (instant instanceof Date) {
+ instant = instant.toTemporalInstant();
+ }
+
+ return instant.toZonedDateTimeISO("UTC");
+}
+
+export function generateTimeStringForFileName(
+ instant?: Temporal.Instant | Date,
+ dateOnly: boolean = false,
+): string {
+ const time = getZonedDateTime(instant);
+
+ if (dateOnly) {
+ return time.toPlainDate().toString();
+ } else {
+ return time.toPlainDateTime().toString().replaceAll(/:|\./g, "-");
+ }
+}
+
+export function transformProperties<T extends object, N>(
+ object: T,
+ transformer: (value: T[keyof T], key: keyof T) => N,
+): { [k in keyof T]: N } {
+ return Object.fromEntries(
+ Object.entries(object).map((
+ [k, v],
+ ) => [k, transformer(v, k as keyof T)]),
+ ) as { [k in keyof T]: N };
+}
+
+export function createSingleton<T>(
+ name: string,
+): [() => T, (v: T | null) => void] {
+ let singleton: T | null = null;
+
+ return [
+ () => {
+ if (singleton == null) {
+ throw new Error(`Singleton ${name} is not set now.`);
+ }
+ return singleton;
+ },
+ (newValue: T | null) => singleton = newValue,
+ ];
+}