diff options
Diffstat (limited to 'services/docker/mail-server/relay/aws')
-rw-r--r-- | services/docker/mail-server/relay/aws/app.ts | 124 | ||||
-rw-r--r-- | services/docker/mail-server/relay/aws/context.ts | 41 | ||||
-rw-r--r-- | services/docker/mail-server/relay/aws/deliver.ts | 52 | ||||
-rw-r--r-- | services/docker/mail-server/relay/aws/mail.ts | 7 | ||||
-rw-r--r-- | services/docker/mail-server/relay/aws/retriever.ts | 100 |
5 files changed, 0 insertions, 324 deletions
diff --git a/services/docker/mail-server/relay/aws/app.ts b/services/docker/mail-server/relay/aws/app.ts deleted file mode 100644 index 9135d21..0000000 --- a/services/docker/mail-server/relay/aws/app.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { parseArgs } from "@std/cli"; -import { z } from "zod"; -import { zValidator } from "@hono/zod-validator"; - -import log from "../log.ts"; -import config from "../config.ts"; -import { AppBase } from "../app.ts"; -import { AwsContext } from "./context.ts"; -import { AwsMailDeliverer } from "./deliver.ts"; -import { AwsMailRetriever } from "./retriever.ts"; - -export class AwsRelayApp extends AppBase { - readonly #aws = new AwsContext(); - readonly #retriever; - protected readonly outboundDeliverer = new AwsMailDeliverer(this.#aws); - - constructor() { - super(); - this.#retriever = new AwsMailRetriever(this.#aws, this.inboundDeliverer); - - this.hono.post( - `/${config.get("awsInboundPath")}`, - async (ctx, next) => { - const auth = ctx.req.header("Authorization"); - if (auth !== config.get("awsInboundKey")) { - return ctx.json({ "msg": "Bad auth!" }, 403); - } - await next(); - }, - zValidator( - "json", - z.object({ - key: z.string(), - recipients: z.array(z.string()).optional(), - }), - ), - async (ctx) => { - const { key, recipients } = ctx.req.valid("json"); - await this.#retriever.deliverS3Mail(key, recipients); - return ctx.json({ "msg": "Done!" }); - }, - ); - } - - realServe() { - this.createCron({ - name: "live-mail-recycler", - interval: 6 * 3600 * 1000, - callback: () => { - return this.#retriever.recycleLiveMails(); - }, - startNow: true, - }); - - return this.serve(); - } - - readonly cli = { - "init": (_: unknown) => { - log.info("Just init!"); - return Promise.resolve(); - }, - "list-lives": async (_: unknown) => { - const liveMails = await this.#retriever.listLiveMails(); - log.info(`Total ${liveMails.length}:`); - log.info(liveMails.join("\n")); - }, - "recycle-lives": async (_: unknown) => { - await this.#retriever.recycleLiveMails(); - }, - "serve": async (_: unknown) => { - await this.serve().http.finished; - }, - "real-serve": async (_: unknown) => { - await this.realServe().http.finished; - }, - } as const; -} - -const nonServerCli = { - "sendmail": async (_: unknown) => { - const decoder = new TextDecoder(); - let text = ""; - for await (const chunk of Deno.stdin.readable) { - text += decoder.decode(chunk); - } - - const res = await fetch( - `http://localhost:${config.HTTP_PORT}/send/raw`, - { - method: "post", - body: text, - }, - ); - log.infoOrError(!res.ok, res); - log.infoOrError(!res.ok, "Body\n" + await res.text()); - if (!res.ok) Deno.exit(-1); - }, -} as const; - -if (import.meta.main) { - const args = parseArgs(Deno.args); - - if (args._.length === 0) { - throw new Error("You must specify a command."); - } - - const command = args._[0]; - - if (command in nonServerCli) { - log.info(`Run non-server command ${command}.`); - await nonServerCli[command as keyof typeof nonServerCli](args); - Deno.exit(0); - } - - const app = new AwsRelayApp(); - if (command in app.cli) { - log.info(`Run command ${command}.`); - await app.cli[command as keyof AwsRelayApp["cli"]](args); - Deno.exit(0); - } else { - throw new Error(command + " is not a valid command."); - } -} diff --git a/services/docker/mail-server/relay/aws/context.ts b/services/docker/mail-server/relay/aws/context.ts deleted file mode 100644 index b1e0336..0000000 --- a/services/docker/mail-server/relay/aws/context.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - CopyObjectCommand, - DeleteObjectCommand, - S3Client, -} from "@aws-sdk/client-s3"; -import { FetchHttpHandler } from "@smithy/fetch-http-handler"; - -import config from "../config.ts"; - -export class AwsContext { - readonly credentials = () => - Promise.resolve({ - accessKeyId: config.get("awsUser"), - secretAccessKey: config.get("awsPassword"), - }); - readonly requestHandler = new FetchHttpHandler(); - - get region() { - return config.get("awsRegion"); - } -} - -export async function s3MoveObject( - client: S3Client, - bucket: string, - path: string, - newPath: string, -): Promise<void> { - const copyCommand = new CopyObjectCommand({ - Bucket: bucket, - Key: newPath, - CopySource: `${bucket}/${path}`, - }); - await client.send(copyCommand); - - const deleteCommand = new DeleteObjectCommand({ - Bucket: bucket, - Key: path, - }); - await client.send(deleteCommand); -} diff --git a/services/docker/mail-server/relay/aws/deliver.ts b/services/docker/mail-server/relay/aws/deliver.ts deleted file mode 100644 index 9c62496..0000000 --- a/services/docker/mail-server/relay/aws/deliver.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2"; - -import { AwsContext } from "./context.ts"; -import { - Mail, - MailDeliverContext, - MailDeliverer, - MailDeliverRecipientResult, -} from "./mail.ts"; -import log from "../log.ts"; - -export class AwsMailDeliverer extends MailDeliverer { - readonly name = "aws"; - readonly #ses; - - constructor(aws: AwsContext) { - super(); - this.#ses = new SESv2Client(aws); - } - - protected override async doDeliver( - mail: Mail, - context: MailDeliverContext, - ): Promise<void> { - log.info("Begin to call aws send-email api..."); - - const result: MailDeliverRecipientResult = { - kind: "done", - message: "Success to call send-email api of aws.", - }; - - try { - const sendCommand = new SendEmailCommand({ - Content: { - Raw: { Data: mail.toUtf8Bytes() }, - }, - }); - - const res = await this.#ses.send(sendCommand); - if (res.MessageId == null) { - log.warn("Aws send-email returns no message id."); - } - mail.awsMessageId = res.MessageId; - } catch (cause) { - result.kind = "fail"; - result.message = "An error was thrown when calling aws send-email." + - cause; - result.cause = cause; - } - context.result.set("*", result); - } -} diff --git a/services/docker/mail-server/relay/aws/mail.ts b/services/docker/mail-server/relay/aws/mail.ts deleted file mode 100644 index a0f2639..0000000 --- a/services/docker/mail-server/relay/aws/mail.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "../mail.ts"; - -declare module "../mail.ts" { - interface Mail { - awsMessageId?: string; - } -} diff --git a/services/docker/mail-server/relay/aws/retriever.ts b/services/docker/mail-server/relay/aws/retriever.ts deleted file mode 100644 index daa3d8c..0000000 --- a/services/docker/mail-server/relay/aws/retriever.ts +++ /dev/null @@ -1,100 +0,0 @@ -/// <reference types="npm:@types/node" /> - -import { - GetObjectCommand, - ListObjectsV2Command, - S3Client, -} from "@aws-sdk/client-s3"; - -import log from "../log.ts"; -import config from "../config.ts"; -import "../better-js.ts"; - -import { Mail, MailDeliverer } from "../mail.ts"; -import { AwsContext, s3MoveObject } from "./context.ts"; - -const AWS_SES_S3_SETUP_TAG = "AMAZON_SES_SETUP_NOTIFICATION"; - -export class AwsMailRetriever { - readonly liveMailPrefix = "mail/live/"; - readonly archiveMailPrefix = "mail/archive/"; - readonly mailBucket = config.get("awsMailBucket"); - - readonly #s3; - - constructor( - aws: AwsContext, - public readonly inboundDeliverer: MailDeliverer, - ) { - this.#s3 = new S3Client(aws); - } - - async listLiveMails(): Promise<string[]> { - log.info("Begin to retrieve live mails."); - - const listCommand = new ListObjectsV2Command({ - Bucket: this.mailBucket, - Prefix: this.liveMailPrefix, - }); - const res = await this.#s3.send(listCommand); - - if (res.Contents == null) { - log.warn("Listing live mails in S3 returns null Content."); - return []; - } - - const result: string[] = []; - for (const object of res.Contents) { - if (object.Key == null) { - log.warn("Listing live mails in S3 returns an object with no Key."); - continue; - } - - if (object.Key.endsWith(AWS_SES_S3_SETUP_TAG)) continue; - - result.push(object.Key.slice(this.liveMailPrefix.length)); - } - return result; - } - - async deliverS3Mail(s3Key: string, recipients: string[] = []) { - log.info(`Begin to deliver s3 mail ${s3Key} to ${recipients.join(" ")}...`); - - log.info(`Fetching s3 mail ${s3Key}...`); - const mailPath = `${this.liveMailPrefix}${s3Key}`; - const command = new GetObjectCommand({ - Bucket: this.mailBucket, - Key: mailPath, - }); - const res = await this.#s3.send(command); - - if (res.Body == null) { - throw new Error("S3 mail returns a null body."); - } - - const rawMail = await res.Body.transformToString(); - log.info(`Done fetching s3 mail ${s3Key}.`); - - log.info(`Delivering s3 mail ${s3Key}...`); - const mail = new Mail(rawMail); - await this.inboundDeliverer.deliver({ mail, recipients: recipients }); - log.info(`Done delivering s3 mail ${s3Key}.`); - - const date = mail.date ?? mail.simpleParseDate(); - const dateString = date?.toFileNameString(true) ?? "invalid-date"; - const newPath = `${this.archiveMailPrefix}${dateString}/${s3Key}`; - - log.info(`Archiving s3 mail ${s3Key} to ${newPath}...`); - await s3MoveObject(this.#s3, this.mailBucket, mailPath, newPath); - log.info(`Done delivering s3 mail ${s3Key}...`); - } - - async recycleLiveMails() { - log.info("Begin to recycle live mails..."); - const mails = await this.listLiveMails(); - log.info(`Found ${mails.length} live mails`); - for (const s3Key of mails) { - await this.deliverS3Mail(s3Key); - } - } -} |