diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-04-30 00:20:23 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-04-30 00:20:54 +0800 |
commit | 84148346e29547cfba92dd697be9bbf31a23a926 (patch) | |
tree | 6750ed1d0ee974cc6144afcc294fd3bdc03defa1 | |
parent | 6aeff979ada4a1775bddb87a0ccad354faaa8093 (diff) | |
download | crupest-84148346e29547cfba92dd697be9bbf31a23a926.tar.gz crupest-84148346e29547cfba92dd697be9bbf31a23a926.tar.bz2 crupest-84148346e29547cfba92dd697be9bbf31a23a926.zip |
HALF WORK!: 2025-4-30
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")); } } |