aboutsummaryrefslogtreecommitdiff
path: root/services/docker
diff options
context:
space:
mode:
Diffstat (limited to 'services/docker')
-rw-r--r--services/docker/mail-server/aws-sendmail/app.ts32
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/app.ts22
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/retriever.ts (renamed from services/docker/mail-server/aws-sendmail/aws/retriver.ts)8
-rw-r--r--services/docker/mail-server/aws-sendmail/aws/service.ts48
-rw-r--r--services/docker/mail-server/aws-sendmail/db.ts30
-rw-r--r--services/docker/mail-server/aws-sendmail/deno.json4
-rw-r--r--services/docker/mail-server/aws-sendmail/deno.lock117
-rw-r--r--services/docker/mail-server/aws-sendmail/mail.ts11
-rw-r--r--services/docker/mail-server/aws-sendmail/main.ts18
-rw-r--r--services/docker/mail-server/aws-sendmail/traffic.ts21
10 files changed, 215 insertions, 96 deletions
diff --git a/services/docker/mail-server/aws-sendmail/app.ts b/services/docker/mail-server/aws-sendmail/app.ts
new file mode 100644
index 0000000..e3554d7
--- /dev/null
+++ b/services/docker/mail-server/aws-sendmail/app.ts
@@ -0,0 +1,32 @@
+import { Hono } from "@hono/hono";
+
+import { Logger, setLogger } from "./logger.ts";
+import { Config, setConfig } from "./config.ts";
+import { DbService } from "./db.ts";
+import { MailDeliverer } from "./mail.ts";
+import { DovecotMailDeliverer } from "./dovecot.ts";
+
+export abstract class AppBase {
+ protected readonly db: DbService;
+ protected readonly localDeliverer: MailDeliverer;
+
+ constructor() {
+ setLogger(new Logger("log"));
+ setConfig(new Config());
+
+ this.db = new DbService();
+ this.localDeliverer = new DovecotMailDeliverer();
+ }
+
+ protected abstract get outboundMailDeliverer(): MailDeliverer;
+ protected setupHono(_hono: Hono): Promise<void> {
+ return Promise.resolve();
+ }
+
+ async run(): Promise<void> {
+ const hono = new Hono();
+ await this.setupHono(hono);
+
+ Deno.serve(hono.fetch);
+ }
+}
diff --git a/services/docker/mail-server/aws-sendmail/aws/app.ts b/services/docker/mail-server/aws-sendmail/aws/app.ts
new file mode 100644
index 0000000..e25c92c
--- /dev/null
+++ b/services/docker/mail-server/aws-sendmail/aws/app.ts
@@ -0,0 +1,22 @@
+import { Hono } from "https://jsr.io/@hono/hono/4.7.9/src/hono.ts";
+import { AppBase } from "../app.ts";
+import { MailDeliverer } from "../mail.ts";
+import { AwsContext } from "./context.ts";
+import { AwsMailDeliverer } from "./deliver.ts";
+
+export class AwsRelayApp extends AppBase {
+ private readonly context = new AwsContext();
+ private readonly deliverer = new AwsMailDeliverer(this.context);
+
+ constructor() {
+ super();
+ }
+
+ protected override setupHono(_hono: Hono): Promise<void> {
+
+ }
+
+ protected override get outboundMailDeliverer(): MailDeliverer {
+ return this.deliverer;
+ }
+}
diff --git a/services/docker/mail-server/aws-sendmail/aws/retriver.ts b/services/docker/mail-server/aws-sendmail/aws/retriever.ts
index b599c15..de577b0 100644
--- a/services/docker/mail-server/aws-sendmail/aws/retriver.ts
+++ b/services/docker/mail-server/aws-sendmail/aws/retriever.ts
@@ -17,7 +17,6 @@ export class AwsMailRetriever {
readonly mailBucket = getConfig().getValue("awsMailBucket");
private readonly s3Client;
- private readonly liveMailRecyclerAborter = new AbortController();
constructor(
aws: AwsContext,
@@ -27,13 +26,6 @@ export class AwsMailRetriever {
this.s3Client = new S3Client({ region, credentials });
}
- setupLiveMailRecycler() {
- Deno.cron("live-mail-recycler", "0 */6 * * *", {
- signal: this.liveMailRecyclerAborter.signal,
- }, () => {
- });
- }
-
async listLiveMails(): Promise<string[]> {
const listCommand = new ListObjectsV2Command({
Bucket: this.mailBucket,
diff --git a/services/docker/mail-server/aws-sendmail/aws/service.ts b/services/docker/mail-server/aws-sendmail/aws/service.ts
new file mode 100644
index 0000000..d0db7ae
--- /dev/null
+++ b/services/docker/mail-server/aws-sendmail/aws/service.ts
@@ -0,0 +1,48 @@
+import { Mail } from "../mail.ts";
+import { AwsContext } from "./context.ts";
+import { AwsMailRetriever } from "./retriever.ts";
+
+export interface AwsServiceSetupOptions {
+ receiveCallback: (mail: Mail) => Promise<void>;
+}
+
+interface Setup {
+ receiveCallback: (mail: Mail) => Promise<void>;
+ retriever: AwsMailRetriever;
+ liveMailRecyclerAborter: AbortController;
+}
+
+export class AwsService implements Disposable {
+ private _setup: Setup | null = null;
+
+ constructor(private readonly aws: AwsContext) {}
+
+ setup(options: AwsServiceSetupOptions): Disposable {
+ if (this._setup != null) {
+ // TODO: Better error.
+ throw new Error("Aws service has already been set up.");
+ }
+ const { receiveCallback } = options;
+ const liveMailRecyclerAborter = new AbortController();
+ const retriever = new AwsMailRetriever(this.aws, receiveCallback);
+
+ Deno.cron("live-mail-recycler", "0 */6 * * *", {
+ signal: liveMailRecyclerAborter.signal,
+ }, () => {
+ retriever.recycleLiveMails();
+ });
+
+ this._setup = {
+ receiveCallback,
+ retriever,
+ liveMailRecyclerAborter,
+ };
+
+ return this;
+ }
+
+ [Symbol.dispose]() {
+ if (this._setup == null) return;
+ this._setup.liveMailRecyclerAborter.abort("Aws service is being disposed.");
+ }
+}
diff --git a/services/docker/mail-server/aws-sendmail/db.ts b/services/docker/mail-server/aws-sendmail/db.ts
index e239e72..e5307be 100644
--- a/services/docker/mail-server/aws-sendmail/db.ts
+++ b/services/docker/mail-server/aws-sendmail/db.ts
@@ -3,6 +3,11 @@
import { SQLocalKysely } from "sqlocal/kysely";
import { Generated, Insertable, Kysely, Migration, Migrator } from "kysely";
+import { Mail } from "./mail.ts";
+
+export class DbError extends Error {
+}
+
const tableNames = {
mail: {
table: "mail",
@@ -10,6 +15,7 @@ const tableNames = {
id: "id",
messageId: "message_id",
awsMessageId: "aws_message_id",
+ date: "date",
raw: "raw",
},
},
@@ -19,6 +25,7 @@ interface MailTable {
[tableNames.mail.columns.id]: Generated<number>;
[tableNames.mail.columns.messageId]: string;
[tableNames.mail.columns.awsMessageId]: string | null;
+ [tableNames.mail.columns.date]: number | null;
[tableNames.mail.columns.raw]: string;
}
@@ -45,6 +52,7 @@ const migrations: Record<string, Migration> = {
(col) => col.notNull().unique(),
)
.addColumn(names.columns.awsMessageId, "text", (col) => col.unique())
+ .addColumn(names.columns.date, "integer")
.addColumn(names.columns.raw, "text", (col) => col.notNull())
.execute();
@@ -96,7 +104,27 @@ export class DbService {
await this._migrator.migrateToLatest();
}
- async addMail(mail: Insertable<MailTable>): Promise<void> {
+ async addMail(mail: Insertable<MailTable> | Mail, options?: {
+ allowNullAwsMessageId?: boolean;
+ }): Promise<void> {
+ if (mail instanceof Mail) {
+ if (mail.messageId == null) {
+ throw new DbError("Mail object has no message id.");
+ }
+ mail = {
+ message_id: mail.messageId,
+ aws_message_id: mail.awsMessageId,
+ date: mail.simpleGetDate()?.getTime(),
+ raw: mail.raw,
+ };
+ }
+ if (
+ mail.aws_message_id == null &&
+ !(options?.allowNullAwsMessageId === true)
+ ) {
+ throw new DbError("Aws message id is missing but it is required.");
+ }
+
await this._db.insertInto(tableNames.mail.table).values(mail)
.executeTakeFirstOrThrow();
}
diff --git a/services/docker/mail-server/aws-sendmail/deno.json b/services/docker/mail-server/aws-sendmail/deno.json
index 67dd7d1..74cbf9c 100644
--- a/services/docker/mail-server/aws-sendmail/deno.json
+++ b/services/docker/mail-server/aws-sendmail/deno.json
@@ -6,10 +6,12 @@
"imports": {
"@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.797.0",
"@aws-sdk/client-sesv2": "npm:@aws-sdk/client-sesv2@^3.782.0",
- "@oak/oak": "jsr:@oak/oak@^17.1.4",
+ "@hono/hono": "jsr:@hono/hono@^4.7.9",
"@std/cli": "jsr:@std/cli@^1.0.17",
"@std/encoding": "jsr:@std/encoding@^1.0.10",
+ "@std/expect": "jsr:@std/expect@^1.0.16",
"@std/path": "jsr:@std/path@^1.0.9",
+ "@std/testing": "jsr:@std/testing@^1.0.12",
"kysely": "npm:kysely@^0.28.2",
"sqlocal": "npm:sqlocal@^0.14.0"
}
diff --git a/services/docker/mail-server/aws-sendmail/deno.lock b/services/docker/mail-server/aws-sendmail/deno.lock
index 81e20d7..3f53c68 100644
--- a/services/docker/mail-server/aws-sendmail/deno.lock
+++ b/services/docker/mail-server/aws-sendmail/deno.lock
@@ -1,75 +1,78 @@
{
- "version": "4",
+ "version": "5",
"specifiers": {
- "jsr:@oak/commons@1": "1.0.0",
- "jsr:@oak/oak@^17.1.4": "17.1.4",
+ "jsr:@hono/hono@^4.7.9": "4.7.9",
"jsr:@std/assert@1": "1.0.13",
- "jsr:@std/bytes@1": "1.0.5",
+ "jsr:@std/assert@^1.0.13": "1.0.13",
+ "jsr:@std/async@^1.0.13": "1.0.13",
"jsr:@std/cli@^1.0.17": "1.0.17",
- "jsr:@std/crypto@1": "1.0.4",
+ "jsr:@std/data-structures@^1.0.8": "1.0.8",
"jsr:@std/encoding@1": "1.0.10",
"jsr:@std/encoding@^1.0.10": "1.0.10",
- "jsr:@std/http@1": "1.0.15",
- "jsr:@std/media-types@1": "1.1.0",
+ "jsr:@std/expect@^1.0.16": "1.0.16",
+ "jsr:@std/fs@^1.0.17": "1.0.17",
+ "jsr:@std/internal@^1.0.6": "1.0.7",
+ "jsr:@std/internal@^1.0.7": "1.0.7",
"jsr:@std/path@1": "1.0.9",
"jsr:@std/path@^1.0.9": "1.0.9",
+ "jsr:@std/testing@^1.0.12": "1.0.12",
"npm:@aws-sdk/client-s3@^3.797.0": "3.797.0",
"npm:@aws-sdk/client-sesv2@^3.782.0": "3.782.0",
"npm:@types/node@*": "22.12.0",
"npm:kysely@~0.28.2": "0.28.2",
- "npm:path-to-regexp@^6.3.0": "6.3.0",
"npm:sqlocal@0.14": "0.14.0_kysely@0.28.2"
},
"jsr": {
- "@oak/commons@1.0.0": {
- "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac",
- "dependencies": [
- "jsr:@std/assert",
- "jsr:@std/bytes",
- "jsr:@std/crypto",
- "jsr:@std/encoding@1",
- "jsr:@std/http",
- "jsr:@std/media-types"
- ]
+ "@hono/hono@4.7.9": {
+ "integrity": "929baffb76209d2ce4c3442bab45957d5dda1726efb82cef13ec3a51d239c543"
},
- "@oak/oak@17.1.4": {
- "integrity": "60530b582bf276ff741e39cc664026781aa08dd5f2bc5134d756cc427bf2c13e",
+ "@std/assert@1.0.13": {
+ "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
"dependencies": [
- "jsr:@oak/commons",
- "jsr:@std/assert",
- "jsr:@std/bytes",
- "jsr:@std/http",
- "jsr:@std/media-types",
- "jsr:@std/path@1",
- "npm:path-to-regexp"
+ "jsr:@std/internal@^1.0.6"
]
},
- "@std/assert@1.0.13": {
- "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29"
- },
- "@std/bytes@1.0.5": {
- "integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e"
+ "@std/async@1.0.13": {
+ "integrity": "1d76ca5d324aef249908f7f7fe0d39aaf53198e5420604a59ab5c035adc97c96"
},
"@std/cli@1.0.17": {
"integrity": "e15b9abe629e17be90cc6216327f03a29eae613365f1353837fa749aad29ce7b"
},
- "@std/crypto@1.0.4": {
- "integrity": "cee245c453bd5366207f4d8aa25ea3e9c86cecad2be3fefcaa6cb17203d79340"
+ "@std/data-structures@1.0.8": {
+ "integrity": "2fb7219247e044c8fcd51341788547575653c82ae2c759ff209e0263ba7d9b66"
},
"@std/encoding@1.0.10": {
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
},
- "@std/http@1.0.15": {
- "integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159",
+ "@std/expect@1.0.16": {
+ "integrity": "ceeef6dda21f256a5f0f083fcc0eaca175428b523359a9b1d9b3a1df11cc7391",
+ "dependencies": [
+ "jsr:@std/assert@^1.0.13",
+ "jsr:@std/internal@^1.0.7"
+ ]
+ },
+ "@std/fs@1.0.17": {
+ "integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b",
"dependencies": [
- "jsr:@std/encoding@^1.0.10"
+ "jsr:@std/path@^1.0.9"
]
},
- "@std/media-types@1.1.0": {
- "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
+ "@std/internal@1.0.7": {
+ "integrity": "39eeb5265190a7bc5d5591c9ff019490bd1f2c3907c044a11b0d545796158a0f"
},
"@std/path@1.0.9": {
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
+ },
+ "@std/testing@1.0.12": {
+ "integrity": "fec973a45ccc62c540fb89296199051fee142409138fd6e3eae409366bcd4720",
+ "dependencies": [
+ "jsr:@std/assert@^1.0.13",
+ "jsr:@std/async",
+ "jsr:@std/data-structures",
+ "jsr:@std/fs",
+ "jsr:@std/internal@^1.0.7",
+ "jsr:@std/path@^1.0.9"
+ ]
}
},
"npm": {
@@ -886,6 +889,9 @@
"@smithy/node-config-provider",
"@smithy/types",
"tslib"
+ ],
+ "optionalPeers": [
+ "aws-crt@>=1.0.0"
]
},
"@aws-sdk/util-user-agent-node@3.796.0": {
@@ -896,6 +902,9 @@
"@smithy/node-config-provider",
"@smithy/types",
"tslib"
+ ],
+ "optionalPeers": [
+ "aws-crt@>=1.0.0"
]
},
"@aws-sdk/xml-builder@3.775.0": {
@@ -1356,7 +1365,8 @@
]
},
"@sqlite.org/sqlite-wasm@3.48.0-build4": {
- "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="
+ "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==",
+ "bin": true
},
"@types/node@22.12.0": {
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
@@ -1379,7 +1389,9 @@
"@ungap/structured-clone",
"@ungap/with-resolvers",
"gc-hook",
- "proxy-target",
+ "proxy-target"
+ ],
+ "optionalDependencies": [
"ws"
]
},
@@ -1387,7 +1399,8 @@
"integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",
"dependencies": [
"strnum"
- ]
+ ],
+ "bin": true
},
"gc-hook@0.3.1": {
"integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A=="
@@ -1395,9 +1408,6 @@
"kysely@0.28.2": {
"integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A=="
},
- "path-to-regexp@6.3.0": {
- "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="
- },
"proxy-target@3.0.2": {
"integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ=="
},
@@ -1407,6 +1417,10 @@
"@sqlite.org/sqlite-wasm",
"coincident",
"kysely"
+ ],
+ "optionalPeers": [
+ "drizzle-orm@*",
+ "kysely"
]
},
"strnum@1.1.2": {
@@ -1419,18 +1433,25 @@
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
},
"uuid@9.0.1": {
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "bin": true
},
"ws@8.18.1": {
- "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="
+ "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+ "optionalPeers": [
+ "bufferutil@^4.0.1",
+ "utf-8-validate@>=5.0.2"
+ ]
}
},
"workspace": {
"dependencies": [
- "jsr:@oak/oak@^17.1.4",
+ "jsr:@hono/hono@^4.7.9",
"jsr:@std/cli@^1.0.17",
"jsr:@std/encoding@^1.0.10",
+ "jsr:@std/expect@^1.0.16",
"jsr:@std/path@^1.0.9",
+ "jsr:@std/testing@^1.0.12",
"npm:@aws-sdk/client-s3@^3.797.0",
"npm:@aws-sdk/client-sesv2@^3.782.0",
"npm:kysely@~0.28.2",
diff --git a/services/docker/mail-server/aws-sendmail/mail.ts b/services/docker/mail-server/aws-sendmail/mail.ts
index 98afcaa..b974e49 100644
--- a/services/docker/mail-server/aws-sendmail/mail.ts
+++ b/services/docker/mail-server/aws-sendmail/mail.ts
@@ -163,14 +163,21 @@ export class Mail {
return headers;
}
- simpleGetDate(): Date | null {
+ /**
+ * Find the Date header and parse it to date.
+ * @param invalidToNull Whether to convert invalid date to null. Default is false.
+ * @returns Parsed date (may be an invalid date if `invalidToNull` us false).
+ * `null` if the date header is missing or parsing fails and `invalidToNull`
+ * is true.
+ */
+ simpleGetDate<T = null>(invalidValue: T | null = null): Date | T | 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;
+ return invalidValue;
}
return date;
}
diff --git a/services/docker/mail-server/aws-sendmail/main.ts b/services/docker/mail-server/aws-sendmail/main.ts
index dcc2af1..490d990 100644
--- a/services/docker/mail-server/aws-sendmail/main.ts
+++ b/services/docker/mail-server/aws-sendmail/main.ts
@@ -1,18 +1,6 @@
-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();
- readonly mailTrafficDeliverer = new MailTrafficHandler(this.aws);
-
- constructor() {
- setLogger(new Logger("log"));
- setConfig(new Config());
- }
-}
+import { AwsRelayApp } from "./aws/app.ts";
if (import.meta.main) {
- const app = new App();
+ const app = new AwsRelayApp();
+ await app.run();
}
diff --git a/services/docker/mail-server/aws-sendmail/traffic.ts b/services/docker/mail-server/aws-sendmail/traffic.ts
deleted file mode 100644
index 7ecc405..0000000
--- a/services/docker/mail-server/aws-sendmail/traffic.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { MailDeliverer } from "./mail.ts";
-import { DbService } from "./db.ts";
-import { DovecotMailDeliverer } from "./dovecot.ts";
-import { AwsContext } from "./aws/context.ts";
-import { AwsMailDeliverer } from "./aws/deliver.ts";
-
-export abstract class MailTrafficHandler {
- constructor(
- public readonly receiver: MailDeliverer,
- public readonly sender: MailDeliverer,
- ) {}
-}
-
-export class AwsRelayTrafficHandler extends MailTrafficHandler {
- constructor(
- private readonly db: DbService,
- private readonly aws: AwsContext,
- ) {
- super(new DovecotMailDeliverer(), new AwsMailDeliverer(aws));
- }
-}