aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuqian Yang <crupest@crupest.life>2025-04-30 00:20:23 +0800
committerYuqian Yang <crupest@crupest.life>2025-04-30 00:20:54 +0800
commit84148346e29547cfba92dd697be9bbf31a23a926 (patch)
tree6750ed1d0ee974cc6144afcc294fd3bdc03defa1
parent6aeff979ada4a1775bddb87a0ccad354faaa8093 (diff)
downloadcrupest-84148346e29547cfba92dd697be9bbf31a23a926.tar.gz
crupest-84148346e29547cfba92dd697be9bbf31a23a926.tar.bz2
crupest-84148346e29547cfba92dd697be9bbf31a23a926.zip
HALF WORK!: 2025-4-30
-rw-r--r--services/docker/mail-server/aws-sendmail/aws.ts6
-rw-r--r--services/docker/mail-server/aws-sendmail/db.ts37
-rw-r--r--services/docker/mail-server/aws-sendmail/deliver.ts46
-rw-r--r--services/docker/mail-server/aws-sendmail/delivers/aws.ts35
-rw-r--r--services/docker/mail-server/aws-sendmail/delivers/dovecot.ts35
-rw-r--r--services/docker/mail-server/aws-sendmail/delivers/traffic.ts9
-rw-r--r--services/docker/mail-server/aws-sendmail/deno.json1
-rw-r--r--services/docker/mail-server/aws-sendmail/deno.lock1
-rw-r--r--services/docker/mail-server/aws-sendmail/logger.ts18
-rw-r--r--services/docker/mail-server/aws-sendmail/mail.ts42
-rw-r--r--services/docker/mail-server/aws-sendmail/main.ts32
11 files changed, 150 insertions, 112 deletions
diff --git a/services/docker/mail-server/aws-sendmail/aws.ts b/services/docker/mail-server/aws-sendmail/aws.ts
index e847cf4..cd1c453 100644
--- a/services/docker/mail-server/aws-sendmail/aws.ts
+++ b/services/docker/mail-server/aws-sendmail/aws.ts
@@ -1,10 +1,8 @@
import { getEnvRequired } from "./base.ts";
-import { Logger } from "./logger.ts";
export class AwsContext {
- constructor(public readonly logger: Logger) {}
-
- readonly awsCredentialsProvider = () => {
+ readonly region = "ap-southeast-1"
+ readonly credentials = () => {
return Promise.resolve(
{
accessKeyId: getEnvRequired("AWS_USER", "aws access key id"),
diff --git a/services/docker/mail-server/aws-sendmail/db.ts b/services/docker/mail-server/aws-sendmail/db.ts
index 05cc7b5..b7e052f 100644
--- a/services/docker/mail-server/aws-sendmail/db.ts
+++ b/services/docker/mail-server/aws-sendmail/db.ts
@@ -1,15 +1,9 @@
// spellchecker: words sqlocal kysely insertable updateable
import { SQLocalKysely } from "sqlocal/kysely";
-import {
- Generated,
- Insertable,
- Kysely,
- Migration,
- Migrator,
- Selectable,
- Updateable,
-} from "kysely";
+import { Generated, Kysely, Migration, Migrator } from "kysely";
+
+import { Mail } from "./mail.ts";
const tableNames = {
mail: {
@@ -18,7 +12,7 @@ const tableNames = {
id: "id",
messageId: "message_id",
awsMessageId: "aws_message_id",
- rawMail: "raw_mail",
+ raw: "raw",
},
},
} as const;
@@ -27,13 +21,9 @@ interface MailTable {
[tableNames.mail.columns.id]: Generated<number>;
[tableNames.mail.columns.messageId]: string;
[tableNames.mail.columns.awsMessageId]: string | null;
- [tableNames.mail.columns.rawMail]: string;
+ [tableNames.mail.columns.raw]: string;
}
-export type Mail = Selectable<MailTable>;
-export type NewMail = Insertable<MailTable>;
-export type MailUpdate = Updateable<MailTable>;
-
interface Database {
[tableNames.mail.table]: MailTable;
}
@@ -57,7 +47,7 @@ const migrations: Record<string, Migration> = {
(col) => col.notNull().unique(),
)
.addColumn(names.columns.awsMessageId, "text", (col) => col.unique())
- .addColumn(names.columns.rawMail, "text", (col) => col.notNull())
+ .addColumn(names.columns.raw, "text", (col) => col.notNull())
.execute();
for (
@@ -101,8 +91,19 @@ export class DbService {
await this._migrator.migrateToLatest();
}
- async addMail(mail: NewMail): Promise<void> {
- await this._db.insertInto(tableNames.mail.table).values(mail)
+ async addMail(mail: Mail): Promise<void> {
+ const { raw, message_id, aws_message_id } = mail;
+ if (message_id == null) {
+ // TODO: Better error.
+ throw new Error(
+ "Failed to add mail to database. Mail has no message id.",
+ );
+ }
+ await this._db.insertInto(tableNames.mail.table).values({
+ raw,
+ message_id,
+ aws_message_id,
+ })
.executeTakeFirstOrThrow();
}
diff --git a/services/docker/mail-server/aws-sendmail/deliver.ts b/services/docker/mail-server/aws-sendmail/deliver.ts
index 7035d8c..e0c6e1c 100644
--- a/services/docker/mail-server/aws-sendmail/deliver.ts
+++ b/services/docker/mail-server/aws-sendmail/deliver.ts
@@ -1,51 +1,61 @@
+import { Mail } from "./mail.ts";
+
class MailDeliverError extends Error {
constructor(
message: string,
options: ErrorOptions,
- public readonly rawMail: string,
+ public readonly mail: Mail,
) {
super(message, options);
}
}
-export class MailDeliverContext {
- constructor(public rawMail: string) {}
-}
-
-type MailDeliverHook<Context> = (context: Context) => Promise<void>;
+type MailDeliverHook = (mail: Mail) => Promise<void>;
-export abstract class MailDeliverer<out TContext extends MailDeliverContext = MailDeliverContext> {
- preHooks: MailDeliverHook<MailDeliverContext>[] = [];
- postHooks: MailDeliverHook<MailDeliverContext>[] = [];
+export abstract class MailDeliverer {
+ preHooks: MailDeliverHook[] = [];
+ postHooks: MailDeliverHook[] = [];
constructor(public readonly destination: string) {}
- protected abstract doPrepare(rawMail: string): Promise<TContext>;
- protected abstract doDeliver(context: TContext): Promise<void>;
+ protected doPrepare(_mail: Mail): Promise<void> {
+ return Promise.resolve();
+ }
+ protected abstract doDeliver(mail: Mail): Promise<void>;
+ protected doFinalize(_mail: Mail): Promise<void> {
+ return Promise.resolve();
+ }
+
+ async deliverRaw(raw: string): Promise<void> {
+ const mail = new Mail(raw);
+ await this.deliver(mail);
+ }
- async deliver(rawMail: string): Promise<void> {
- const context = await this.doPrepare(rawMail);
+ async deliver(mail: Mail): Promise<void> {
+ this.doPrepare(mail);
for (const hook of this.preHooks) {
- await hook(context);
+ await hook(mail);
}
- await this.doDeliver(context);
+ await this.doDeliver(mail);
for (const hook of this.postHooks) {
- await hook(context);
+ await hook(mail);
}
+
+ await this.doFinalize(mail);
}
protected throwError(
reason: string,
- rawMail: string,
+ mail: Mail,
cause?: unknown,
): never {
throw new MailDeliverError(
`Failed to deliver mail to ${this.destination}: ${reason}`,
{ cause },
- rawMail,
+ mail,
);
}
}
diff --git a/services/docker/mail-server/aws-sendmail/delivers/aws.ts b/services/docker/mail-server/aws-sendmail/delivers/aws.ts
index 85d86ec..9fe7bec 100644
--- a/services/docker/mail-server/aws-sendmail/delivers/aws.ts
+++ b/services/docker/mail-server/aws-sendmail/delivers/aws.ts
@@ -1,28 +1,31 @@
-import { SESv2Client } from "@aws-sdk/client-sesv2";
+import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2";
import { AwsContext } from "../aws.ts";
-import { MailDeliverer, MailDeliverContext } from "../deliver.ts";
+import { Mail } from "../mail.ts";
+import { MailDeliverer } from "../deliver.ts";
-export class AwsMailDeliverContext extends MailDeliverContext {
- awsMessageId: string | null = null;
-
- constructor(rawMail: string) {
- super(rawMail);
- }
-}
-
-class AwsMailDeliverer extends MailDeliverer<AwsMailDeliverContext> {
+export class AwsMailDeliverer extends MailDeliverer {
private _ses;
constructor(readonly aws: AwsContext) {
super("aws");
- this._ses = new SESv2Client({ credentials: aws.awsCredentialsProvider });
- }
- protected override doPrepare(rawMail: string): Promise<AwsMailDeliverContext> {
- return Promise.resolve(new AwsMailDeliverContext(rawMail))
+ const { region, credentials } = aws;
+
+ this._ses = new SESv2Client({ region, credentials });
}
- protected override async doDeliver(context: AwsContext): Promise<void> {
+ protected override async doDeliver(mail: Mail): Promise<void> {
+ const sendCommand = new SendEmailCommand({
+ Content: {
+ Raw: { Data: mail.encodeUtf8() },
+ },
+ });
+
+ const res = await this._ses.send(sendCommand);
+ if (res.MessageId == null) {
+ throw Error("No message id is returned from aws.");
+ }
+ mail.aws_message_id = res.MessageId;
}
}
diff --git a/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts b/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts
index e30c558..2b35872 100644
--- a/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts
+++ b/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts
@@ -1,31 +1,20 @@
-import { Logger } from "../logger.ts";
-import { MailDeliverContext, MailDeliverer } from "../deliver.ts";
+import { getLogger } from "../logger.ts";
+import { MailDeliverer } from "../deliver.ts";
+import { Mail } from "../mail.ts";
export class DovecotMailDeliverer extends MailDeliverer {
- constructor(private readonly logger: Logger) {
+ constructor() {
super("dovecot");
}
readonly ldaBin = "dovecot-lda";
- protected override doPrepare(
- rawMail: string,
- ): Promise<MailDeliverContext> {
- return Promise.resolve(new MailDeliverContext(rawMail));
- }
-
- protected override async doDeliver(
- context: MailDeliverContext,
- ): Promise<void> {
- const { logger, ldaBin } = this;
- const { rawMail } = context;
+ protected override async doDeliver(mail: Mail): Promise<void> {
+ const { ldaBin } = this;
let status;
try {
- const utf8Encoder = new TextEncoder();
- // TODO: A problem here is if mail is VERY long, this will block for a long time.
- // Maybe some task queue can be used.
- const utf8Stream = utf8Encoder.encode(rawMail);
+ const utf8Stream = mail.encodeUtf8();
const ldaCommand = new Deno.Command(ldaBin, {
stdin: "piped",
@@ -33,7 +22,7 @@ export class DovecotMailDeliverer extends MailDeliverer {
stderr: "piped",
});
const ldaProcess = ldaCommand.spawn();
- logger.logProgramOutput(ldaProcess, ldaBin);
+ getLogger().logProgramOutput(ldaProcess, ldaBin);
const stdinWriter = ldaProcess.stdin.getWriter();
await stdinWriter.ready;
@@ -42,15 +31,11 @@ export class DovecotMailDeliverer extends MailDeliverer {
status = await ldaProcess.status;
} catch (cause) {
- this.throwError(
- "external error.",
- rawMail,
- cause,
- );
+ this.throwError("external error.", mail, cause);
}
if (!status.success) {
- this.throwError(`${ldaBin} exited with non-zero.`, rawMail);
+ this.throwError(`${ldaBin} exited with non-zero.`, mail);
}
}
}
diff --git a/services/docker/mail-server/aws-sendmail/delivers/traffic.ts b/services/docker/mail-server/aws-sendmail/delivers/traffic.ts
index a3ff52b..3d567f9 100644
--- a/services/docker/mail-server/aws-sendmail/delivers/traffic.ts
+++ b/services/docker/mail-server/aws-sendmail/delivers/traffic.ts
@@ -1,11 +1,14 @@
-import { Logger } from "../logger.ts";
import { MailDeliverer } from "../deliver.ts";
import { DovecotMailDeliverer } from "./dovecot.ts";
+import { AwsContext } from "../aws.ts";
+import { AwsMailDeliverer } from "./aws.ts";
export class MailTrafficDeliverer {
receiver: MailDeliverer;
+ sender: MailDeliverer;
- constructor(logger: Logger) {
- this.receiver = new DovecotMailDeliverer(logger);
+ constructor(aws: AwsContext) {
+ this.receiver = new DovecotMailDeliverer();
+ this.sender = new AwsMailDeliverer(aws);
}
}
diff --git a/services/docker/mail-server/aws-sendmail/deno.json b/services/docker/mail-server/aws-sendmail/deno.json
index 81318fa..67dd7d1 100644
--- a/services/docker/mail-server/aws-sendmail/deno.json
+++ b/services/docker/mail-server/aws-sendmail/deno.json
@@ -8,6 +8,7 @@
"@aws-sdk/client-sesv2": "npm:@aws-sdk/client-sesv2@^3.782.0",
"@oak/oak": "jsr:@oak/oak@^17.1.4",
"@std/cli": "jsr:@std/cli@^1.0.17",
+ "@std/encoding": "jsr:@std/encoding@^1.0.10",
"@std/path": "jsr:@std/path@^1.0.9",
"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 2567551..2731eb5 100644
--- a/services/docker/mail-server/aws-sendmail/deno.lock
+++ b/services/docker/mail-server/aws-sendmail/deno.lock
@@ -1419,6 +1419,7 @@
"dependencies": [
"jsr:@oak/oak@^17.1.4",
"jsr:@std/cli@^1.0.17",
+ "jsr:@std/encoding@^1.0.10",
"jsr:@std/path@^1.0.9",
"npm:@aws-sdk/client-s3@^3.797.0",
"npm:@aws-sdk/client-sesv2@^3.782.0",
diff --git a/services/docker/mail-server/aws-sendmail/logger.ts b/services/docker/mail-server/aws-sendmail/logger.ts
index 12dbc80..8cea3b0 100644
--- a/services/docker/mail-server/aws-sendmail/logger.ts
+++ b/services/docker/mail-server/aws-sendmail/logger.ts
@@ -13,7 +13,10 @@ function generateTimeStringForFileName(
}
export class Logger {
- constructor(public readonly path: string) {
+ constructor(public readonly path: string) {}
+
+ warn(message: string) {
+ console.log(message);
}
generateLogFilePath(
@@ -53,3 +56,16 @@ export class Logger {
process.stderr.pipeTo(stderrFile.writable);
}
}
+
+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;
+}
diff --git a/services/docker/mail-server/aws-sendmail/mail.ts b/services/docker/mail-server/aws-sendmail/mail.ts
new file mode 100644
index 0000000..e673593
--- /dev/null
+++ b/services/docker/mail-server/aws-sendmail/mail.ts
@@ -0,0 +1,42 @@
+import { encodeBase64 } from "@std/encoding/base64";
+import { getLogger } from "./logger.ts";
+
+export class Mail {
+ message_id: string | null = null;
+ aws_message_id: string | null = null;
+
+ constructor(public raw: string) {}
+
+ encodeUtf8(): Uint8Array {
+ const utf8Encoder = new TextEncoder();
+ // TODO: A problem here is if mail is VERY long, this will block for a long time.
+ // Maybe some task queue can be used.
+ return utf8Encoder.encode(this.raw);
+ }
+
+ getRawBase64(): string {
+ return encodeBase64(this.raw);
+ }
+
+ appendHeaders(
+ rawMail: string,
+ headers: [key: string, value: string][],
+ ): string {
+ const separatorMatch = rawMail.match(/(\r\n|\n)(\r\n|\n)/);
+ if (separatorMatch == null) {
+ throw new Error(
+ "No header/body separator (2 successive EOLs) found. Cannot append headers.",
+ );
+ }
+
+ if (separatorMatch[1] !== separatorMatch[2]) {
+ getLogger().warn("Different EOLs (\\r\\n and \\n) found in mail!");
+ }
+
+ const headerStr = headers.map(([k, v]) => `${k}: ${v}${separatorMatch[1]}`)
+ .join("");
+ const endOfHeadersIndex = separatorMatch.index! + separatorMatch[1].length;
+ return rawMail.slice(0, endOfHeadersIndex) + headerStr +
+ rawMail.slice(endOfHeadersIndex);
+ }
+}
diff --git a/services/docker/mail-server/aws-sendmail/main.ts b/services/docker/mail-server/aws-sendmail/main.ts
index 3a73a6f..98e364e 100644
--- a/services/docker/mail-server/aws-sendmail/main.ts
+++ b/services/docker/mail-server/aws-sendmail/main.ts
@@ -1,5 +1,6 @@
+import { AwsContext } from "./aws.ts";
import { MailTrafficDeliverer } from "./delivers/traffic.ts";
-import { Logger } from "./logger.ts";
+import { Logger, setLogger } from "./logger.ts";
class BugError extends Error {
}
@@ -7,35 +8,12 @@ class BugError extends Error {
function warn(message: string) {
}
-class MailProcessor {
- appendHeaders(
- rawMail: string,
- headers: [key: string, value: string][],
- ): string {
- const separatorMatch = rawMail.match(/(\r\n|\n)(\r\n|\n)/);
- if (separatorMatch == null) {
- throw new Error(
- "No header/body separator (2 successive EOLs) found. Cannot append headers.",
- );
- }
-
- if (separatorMatch[1] !== separatorMatch[2]) {
- warn("Different EOLs (\\r\\n and \\n) found in mail!");
- }
-
- const headerStr = headers.map(([k, v]) => `${k}: ${v}${separatorMatch[1]}`)
- .join("");
- const endOfHeadersIndex = separatorMatch.index! + separatorMatch[1].length;
- return rawMail.slice(0, endOfHeadersIndex) + headerStr +
- rawMail.slice(endOfHeadersIndex);
- }
-}
-
class App {
- readonly logger = new Logger("log");
- readonly mailTrafficDeliverer = new MailTrafficDeliverer(this.logger);
+ readonly aws = new AwsContext();
+ readonly mailTrafficDeliverer = new MailTrafficDeliverer(this.aws);
constructor() {
+ setLogger(new Logger("log"));
}
}