diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-04-30 00:20:23 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-05-14 17:14:29 +0800 |
commit | 8930bb11fce19c773201b4f499d99c1c3c28efa6 (patch) | |
tree | 5ad13f3fc5fc7c6bd914aad39e0ffc824cc0d612 | |
parent | 8ba08870fdb3bafa7b8739c4f1c57a70b8780143 (diff) | |
download | crupest-8930bb11fce19c773201b4f499d99c1c3c28efa6.tar.gz crupest-8930bb11fce19c773201b4f499d99c1c3c28efa6.tar.bz2 crupest-8930bb11fce19c773201b4f499d99c1c3c28efa6.zip |
HALF WORK!: 2025-5-14 2mail
-rw-r--r-- | services/docker/mail-server/aws-sendmail/app.ts | 32 | ||||
-rw-r--r-- | services/docker/mail-server/aws-sendmail/aws/app.ts | 22 | ||||
-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.ts | 48 | ||||
-rw-r--r-- | services/docker/mail-server/aws-sendmail/db.ts | 30 | ||||
-rw-r--r-- | services/docker/mail-server/aws-sendmail/deno.json | 4 | ||||
-rw-r--r-- | services/docker/mail-server/aws-sendmail/deno.lock | 117 | ||||
-rw-r--r-- | services/docker/mail-server/aws-sendmail/mail.ts | 11 | ||||
-rw-r--r-- | services/docker/mail-server/aws-sendmail/main.ts | 18 | ||||
-rw-r--r-- | services/docker/mail-server/aws-sendmail/traffic.ts | 21 |
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)); - } -} |