From 690f9f843324c359a46bcb8b51b5563e2bec800a Mon Sep 17 00:00:00 2001 From: Yuqian Yang Date: Wed, 30 Apr 2025 00:20:23 +0800 Subject: HALF WORK!: 2025-5-4 --- services/docker/mail-server/aws-sendmail/aws.ts | 16 ---- .../docker/mail-server/aws-sendmail/aws/base.ts | 15 ++++ .../docker/mail-server/aws-sendmail/aws/deliver.ts | 42 ++++++++++ .../mail-server/aws-sendmail/aws/retriver.ts | 89 ++++++++++++++++++++++ services/docker/mail-server/aws-sendmail/base.ts | 35 +++++++-- .../mail-server/aws-sendmail/delivers/aws.ts | 42 ---------- .../mail-server/aws-sendmail/delivers/dovecot.ts | 40 ---------- services/docker/mail-server/aws-sendmail/deno.lock | 10 +++ .../docker/mail-server/aws-sendmail/dovecot.ts | 40 ++++++++++ services/docker/mail-server/aws-sendmail/logger.ts | 11 +-- services/docker/mail-server/aws-sendmail/main.ts | 2 +- .../docker/mail-server/aws-sendmail/traffic.ts | 6 +- 12 files changed, 230 insertions(+), 118 deletions(-) delete mode 100644 services/docker/mail-server/aws-sendmail/aws.ts create mode 100644 services/docker/mail-server/aws-sendmail/aws/base.ts create mode 100644 services/docker/mail-server/aws-sendmail/aws/deliver.ts create mode 100644 services/docker/mail-server/aws-sendmail/aws/retriver.ts delete mode 100644 services/docker/mail-server/aws-sendmail/delivers/aws.ts delete mode 100644 services/docker/mail-server/aws-sendmail/delivers/dovecot.ts create mode 100644 services/docker/mail-server/aws-sendmail/dovecot.ts diff --git a/services/docker/mail-server/aws-sendmail/aws.ts b/services/docker/mail-server/aws-sendmail/aws.ts deleted file mode 100644 index cd1c453..0000000 --- a/services/docker/mail-server/aws-sendmail/aws.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getEnvRequired } from "./base.ts"; - -export class AwsContext { - readonly region = "ap-southeast-1" - readonly credentials = () => { - return Promise.resolve( - { - accessKeyId: getEnvRequired("AWS_USER", "aws access key id"), - secretAccessKey: getEnvRequired( - "AWS_PASSWORD", - "aws secret access key", - ), - } as const, - ); - }; -} diff --git a/services/docker/mail-server/aws-sendmail/aws/base.ts b/services/docker/mail-server/aws-sendmail/aws/base.ts new file mode 100644 index 0000000..1e23009 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/aws/base.ts @@ -0,0 +1,15 @@ +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/deliver.ts b/services/docker/mail-server/aws-sendmail/aws/deliver.ts new file mode 100644 index 0000000..bdc9584 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/aws/deliver.ts @@ -0,0 +1,42 @@ +import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2"; + +import { AwsContext } from "./base.ts"; +import { Mail, MailDeliverer } from "../mail.ts"; + +export class AwsMailDeliverer extends MailDeliverer { + private _ses; + + constructor(readonly aws: AwsContext) { + super("aws"); + + const { region, credentials } = aws; + + this._ses = new SESv2Client({ region, credentials }); + } + + protected override async doDeliver(mail: Mail): Promise { + let awsMessageId: string | undefined; + + try { + const sendCommand = new SendEmailCommand({ + Content: { + Raw: { Data: mail.encodeUtf8() }, + }, + }); + + const res = await this._ses.send(sendCommand); + awsMessageId = res.MessageId; + } catch (cause) { + mail.throwDeliverError( + this, + "failed to call send-email api of aws.", + cause, + ); + } + + if (awsMessageId == null) { + mail.setDelivered(this, new Error("No message id is returned from aws.")); + } + mail.awsMessageId = awsMessageId ?? null; + } +} diff --git a/services/docker/mail-server/aws-sendmail/aws/retriver.ts b/services/docker/mail-server/aws-sendmail/aws/retriver.ts new file mode 100644 index 0000000..cdfe6f1 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/aws/retriver.ts @@ -0,0 +1,89 @@ +/// + +import { + GetObjectCommand, + ListObjectsV2Command, + S3Client, +} from "@aws-sdk/client-s3"; + +import { generateTimeStringForFileName, getEnvRequired } from "../base.ts"; +import { getLogger } from "../logger.ts"; +import { AwsContext } from "./base.ts"; +import { MailDeliverer } from "../mail.ts"; + +export class AwsMailRetriever { + mailBucket = getEnvRequired( + "AWS_MAIL_BUCKET", + "aws s3 bucket saving raw mails", + ); + liveMailPrefix = "mail/live/"; + archiveMailPrefix = "mail/archive/"; + + private s3Client; + private liveMailRecyclerAborter = new AbortController(); + + constructor(private aws: AwsContext, private localDeliverer: MailDeliverer) { + const { region, credentials } = aws; + this.s3Client = new S3Client({ region, credentials }); + } + + setupLiveMailRecycler() { + Deno.cron("live-mail-recycler", "0 */6 * * *", { + signal: this.liveMailRecyclerAborter.signal, + }, () => { + }); + } + + generateArchivePrefix(instant: Date | Temporal.Instant): string { + return `${this.archiveMailPrefix}${ + generateTimeStringForFileName(instant, true) + }/`; + } + + async listLiveMails(): Promise { + const listCommand = new ListObjectsV2Command({ + Bucket: this.mailBucket, + Prefix: this.liveMailPrefix, + }); + const res = await this.s3Client.send(listCommand); + + if (res.Contents == null) { + getLogger().warn("Listing live mails in S3 returns null Content."); + return []; + } + + const result: string[] = []; + for (const object of res.Contents) { + if (object.Key != null) { + result.push(object.Key); + } else { + getLogger().warn( + "Listing live mails in S3 returns an object with no Key.", + ); + } + } + return result; + } + + async deliverS3MailObject(messageId: string) { + const command = new GetObjectCommand({ + Bucket: this.mailBucket, + Key: `${this.liveMailPrefix}${messageId}`, + }); + const res = await this.s3Client.send(command); + + if (res.Body == null) { + // TODO: Better error. + throw new Error(); + } + + const rawMail = await res.Body.transformToString(); + await this.localDeliverer.deliverRaw(rawMail); + + const archiveCommand = new + } + + async recycleLiveMails() { + const mails = await this.listLiveMails(); + } +} diff --git a/services/docker/mail-server/aws-sendmail/base.ts b/services/docker/mail-server/aws-sendmail/base.ts index 97d377b..08b592a 100644 --- a/services/docker/mail-server/aws-sendmail/base.ts +++ b/services/docker/mail-server/aws-sendmail/base.ts @@ -1,11 +1,34 @@ -export const APP_PREFIX="crupest" -export const APP_NAME="mailserver" +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) + 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}.`) + 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, "-"); } - return value } diff --git a/services/docker/mail-server/aws-sendmail/delivers/aws.ts b/services/docker/mail-server/aws-sendmail/delivers/aws.ts deleted file mode 100644 index ecc9956..0000000 --- a/services/docker/mail-server/aws-sendmail/delivers/aws.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2"; - -import { AwsContext } from "../aws.ts"; -import { Mail, MailDeliverer } from "../mail.ts"; - -export class AwsMailDeliverer extends MailDeliverer { - private _ses; - - constructor(readonly aws: AwsContext) { - super("aws"); - - const { region, credentials } = aws; - - this._ses = new SESv2Client({ region, credentials }); - } - - protected override async doDeliver(mail: Mail): Promise { - let awsMessageId: string | undefined; - - try { - const sendCommand = new SendEmailCommand({ - Content: { - Raw: { Data: mail.encodeUtf8() }, - }, - }); - - const res = await this._ses.send(sendCommand); - awsMessageId = res.MessageId; - } catch (cause) { - mail.throwDeliverError( - this, - "failed to call send-email api of aws.", - cause, - ); - } - - if (awsMessageId == null) { - mail.setDelivered(this, new Error("No message id is returned from aws.")); - } - mail.awsMessageId = awsMessageId ?? null; - } -} diff --git a/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts b/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts deleted file mode 100644 index 1b8c911..0000000 --- a/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getLogger } from "../logger.ts"; -import { Mail, MailDeliverer } from "../mail.ts"; - -export class DovecotMailDeliverer extends MailDeliverer { - constructor() { - super("dovecot"); - } - - readonly ldaBin = "dovecot-lda"; - - protected override async doDeliver(mail: Mail): Promise { - const { ldaBin } = this; - let status; - - try { - const utf8Stream = mail.encodeUtf8(); - - const ldaCommand = new Deno.Command(ldaBin, { - stdin: "piped", - stdout: "piped", - stderr: "piped", - }); - const ldaProcess = ldaCommand.spawn(); - getLogger().logProgramOutput(ldaProcess, ldaBin); - - const stdinWriter = ldaProcess.stdin.getWriter(); - await stdinWriter.ready; - await stdinWriter.write(utf8Stream); - await stdinWriter.ready; - - status = await ldaProcess.status; - } catch (cause) { - mail.throwDeliverError(this, "external error.", cause); - } - - if (!status.success) { - mail.throwDeliverError(this, `${ldaBin} exited with non-zero.`); - } - } -} diff --git a/services/docker/mail-server/aws-sendmail/deno.lock b/services/docker/mail-server/aws-sendmail/deno.lock index 2731eb5..81e20d7 100644 --- a/services/docker/mail-server/aws-sendmail/deno.lock +++ b/services/docker/mail-server/aws-sendmail/deno.lock @@ -15,6 +15,7 @@ "jsr:@std/path@^1.0.9": "1.0.9", "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" @@ -1357,6 +1358,12 @@ "@sqlite.org/sqlite-wasm@3.48.0-build4": { "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==" }, + "@types/node@22.12.0": { + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "dependencies": [ + "undici-types" + ] + }, "@ungap/structured-clone@1.3.0": { "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" }, @@ -1408,6 +1415,9 @@ "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "undici-types@6.20.0": { + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, "uuid@9.0.1": { "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, diff --git a/services/docker/mail-server/aws-sendmail/dovecot.ts b/services/docker/mail-server/aws-sendmail/dovecot.ts new file mode 100644 index 0000000..520f93b --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/dovecot.ts @@ -0,0 +1,40 @@ +import { getLogger } from "./logger.ts"; +import { Mail, MailDeliverer } from "./mail.ts"; + +export class DovecotMailDeliverer extends MailDeliverer { + constructor() { + super("dovecot"); + } + + readonly ldaBin = "dovecot-lda"; + + protected override async doDeliver(mail: Mail): Promise { + const { ldaBin } = this; + let status; + + try { + const utf8Stream = mail.encodeUtf8(); + + const ldaCommand = new Deno.Command(ldaBin, { + stdin: "piped", + stdout: "piped", + stderr: "piped", + }); + const ldaProcess = ldaCommand.spawn(); + getLogger().logProgramOutput(ldaProcess, ldaBin); + + const stdinWriter = ldaProcess.stdin.getWriter(); + await stdinWriter.ready; + await stdinWriter.write(utf8Stream); + await stdinWriter.ready; + + status = await ldaProcess.status; + } catch (cause) { + mail.throwDeliverError(this, "external error.", cause); + } + + if (!status.success) { + mail.throwDeliverError(this, `${ldaBin} exited with non-zero.`); + } + } +} diff --git a/services/docker/mail-server/aws-sendmail/logger.ts b/services/docker/mail-server/aws-sendmail/logger.ts index 8cea3b0..3a0cff6 100644 --- a/services/docker/mail-server/aws-sendmail/logger.ts +++ b/services/docker/mail-server/aws-sendmail/logger.ts @@ -1,16 +1,7 @@ import * as path from "@std/path"; +import { generateTimeStringForFileName } from "./base.ts"; -function generateTimeStringForFileName( - instant?: Temporal.Instant | Date, -): string { - if (instant == null) { - instant = Temporal.Now.instant(); - } else if (instant instanceof Date) { - instant = instant.toTemporalInstant(); - } - return instant.toString().replaceAll(/:|\./g, "-"); -} export class Logger { constructor(public readonly path: string) {} diff --git a/services/docker/mail-server/aws-sendmail/main.ts b/services/docker/mail-server/aws-sendmail/main.ts index 7702c75..7207188 100644 --- a/services/docker/mail-server/aws-sendmail/main.ts +++ b/services/docker/mail-server/aws-sendmail/main.ts @@ -1,4 +1,4 @@ -import { AwsContext } from "./aws.ts"; +import { AwsContext } from "./aws/base.ts"; import { MailTrafficHandler } from "./traffic.ts"; import { Logger, setLogger } from "./logger.ts"; diff --git a/services/docker/mail-server/aws-sendmail/traffic.ts b/services/docker/mail-server/aws-sendmail/traffic.ts index f31e9ae..4e92557 100644 --- a/services/docker/mail-server/aws-sendmail/traffic.ts +++ b/services/docker/mail-server/aws-sendmail/traffic.ts @@ -1,8 +1,8 @@ import { MailDeliverer } from "./mail.ts"; -import { AwsContext } from "./aws.ts"; import { DbService } from "./db.ts"; -import { DovecotMailDeliverer } from "./delivers/dovecot.ts"; -import { AwsMailDeliverer } from "./delivers/aws.ts"; +import { DovecotMailDeliverer } from "./dovecot.ts"; +import { AwsContext } from "./aws/base.ts"; +import { AwsMailDeliverer } from "./aws/deliver.ts"; export abstract class MailTrafficHandler { constructor( -- cgit v1.2.3