diff options
Diffstat (limited to 'deno/mail-relay/aws')
-rw-r--r-- | deno/mail-relay/aws/app.ts | 157 | ||||
-rw-r--r-- | deno/mail-relay/aws/deliver.ts | 13 | ||||
-rw-r--r-- | deno/mail-relay/aws/fetch.ts | 42 | ||||
-rw-r--r-- | deno/mail-relay/aws/mail.ts | 22 |
4 files changed, 109 insertions, 125 deletions
diff --git a/deno/mail-relay/aws/app.ts b/deno/mail-relay/aws/app.ts index 685d7a9..cb275ae 100644 --- a/deno/mail-relay/aws/app.ts +++ b/deno/mail-relay/aws/app.ts @@ -1,11 +1,12 @@ import { join } from "@std/path"; -import { parseArgs } from "@std/cli"; import { z } from "zod"; import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { FetchHttpHandler } from "@smithy/fetch-http-handler"; +// @ts-types="npm:@types/yargs" +import yargs from "yargs"; -import { Logger } from "@crupest/base/log"; +import { LogFileProvider } from "@crupest/base/log"; import { ConfigDefinition, ConfigProvider } from "@crupest/base/config"; import { CronTask } from "@crupest/base/cron"; @@ -17,7 +18,7 @@ import { } from "./mail.ts"; import { AwsMailDeliverer } from "./deliver.ts"; import { AwsMailFetcher, AwsS3MailConsumer } from "./fetch.ts"; -import { createInbound, createHono, sendMail, createSmtp } from "../app.ts"; +import { createHono, createInbound, createSmtp, sendMail } from "../app.ts"; const PREFIX = "crupest-mail-server"; const CONFIG_DEFINITIONS = { @@ -93,17 +94,16 @@ function createAwsOptions({ } function createOutbound( - logger: Logger, awsOptions: ReturnType<typeof createAwsOptions>, db: DbService, ) { - const deliverer = new AwsMailDeliverer(logger, awsOptions); + const deliverer = new AwsMailDeliverer(awsOptions); deliverer.preHooks.push( new AwsMailMessageIdRewriteHook(db.messageIdToAws.bind(db)), ); deliverer.postHooks.push( new AwsMailMessageIdSaveHook((original, aws) => - db.addMessageIdMap({ message_id: original, aws_message_id: aws }).then(), + db.addMessageIdMap({ message_id: original, aws_message_id: aws }).then() ), ); return deliverer; @@ -155,80 +155,70 @@ function createCron(fetcher: AwsMailFetcher, consumer: AwsS3MailConsumer) { function createBaseServices() { const config = new ConfigProvider(PREFIX, CONFIG_DEFINITIONS); Deno.mkdirSync(config.get("dataPath"), { recursive: true }); - const logger = new Logger(); - logger.externalLogDir = join(config.get("dataPath"), "log"); - return { config, logger }; + const logFileProvider = new LogFileProvider( + join(config.get("dataPath"), "log"), + ); + return { config, logFileProvider }; } function createAwsFetchOnlyServices() { - const { config, logger } = createBaseServices(); + const services = createBaseServices(); + const { config } = services; + const awsOptions = createAwsOptions({ user: config.get("awsUser"), password: config.get("awsPassword"), region: config.get("awsRegion"), }); - const fetcher = new AwsMailFetcher( - logger, - awsOptions, - config.get("awsMailBucket"), - ); - return { config, logger, awsOptions, fetcher }; + const fetcher = new AwsMailFetcher(awsOptions, config.get("awsMailBucket")); + + return { ...services, awsOptions, fetcher }; } function createAwsRecycleOnlyServices() { - const { config, logger, awsOptions, fetcher } = createAwsFetchOnlyServices(); + const services = createAwsFetchOnlyServices(); + const { config, logFileProvider } = services; - const inbound = createInbound(logger, { + const inbound = createInbound(logFileProvider, { fallback: config.getList("inboundFallback"), ldaPath: config.get("ldaPath"), aliasFile: join(config.get("dataPath"), "aliases.csv"), mailDomain: config.get("mailDomain"), }); - const recycler = (rawMail: string, _: unknown): Promise<void> => inbound.deliver({ mail: new Mail(rawMail) }).then(); - return { config, logger, awsOptions, fetcher, inbound, recycler }; + return { ...services, inbound, recycler }; } function createAwsServices() { - const { config, logger, inbound, awsOptions, fetcher, recycler } = - createAwsRecycleOnlyServices(); + const services = createAwsRecycleOnlyServices(); + const { config, awsOptions } = services; + const dbService = new DbService(join(config.get("dataPath"), "db.sqlite")); - const outbound = createOutbound(logger, awsOptions, dbService); + const outbound = createOutbound(awsOptions, dbService); - return { - config, - logger, - inbound, - dbService, - awsOptions, - fetcher, - recycler, - outbound, - }; + return { ...services, dbService, outbound }; } function createServerServices() { const services = createAwsServices(); - const { logger, config, outbound, inbound, fetcher } = services; - const smtp = createSmtp(logger, outbound); + const { config, outbound, inbound, fetcher } = services; - const hono = createHono(logger, outbound, inbound); + const smtp = createSmtp(outbound); + const hono = createHono(outbound, inbound); setupAwsHono(hono, { path: config.get("awsInboundPath"), auth: config.get("awsInboundKey"), callback: (s3Key, recipients) => { - return fetcher.consumeS3Mail(s3Key, (rawMail, _) => - inbound.deliver({ mail: new Mail(rawMail), recipients }).then(), + return fetcher.consumeS3Mail( + s3Key, + (rawMail, _) => + inbound.deliver({ mail: new Mail(rawMail), recipients }).then(), ); }, }); - return { - ...services, - smtp, - hono, - }; + return { ...services, smtp, hono }; } function serve(cron: boolean = false) { @@ -251,10 +241,12 @@ function serve(cron: boolean = false) { } async function listLives() { - const { logger, fetcher } = createAwsFetchOnlyServices(); + const { fetcher } = createAwsFetchOnlyServices(); const liveMails = await fetcher.listLiveMails(); - logger.info(`Total ${liveMails.length}:`); - logger.info(liveMails.join("\n")); + console.info(`Total ${liveMails.length}:`); + if (liveMails.length !== 0) { + console.info(liveMails.join("\n")); + } } async function recycleLives() { @@ -263,38 +255,43 @@ async function recycleLives() { } if (import.meta.main) { - const args = parseArgs(Deno.args); - - if (args._.length === 0) { - throw new Error("You must specify a command."); - } - - const command = String(args._[0]); - - switch (command) { - case "sendmail": { - const { logger, config } = createBaseServices(); - await sendMail(logger, config.getInt("httpPort")); - break; - } - case "list-lives": { - await listLives(); - break; - } - case "recycle-lives": { - await recycleLives(); - break; - } - case "serve": { - serve(); - break; - } - case "real-serve": { - serve(true); - break; - } - default: { - throw new Error(command + " is not a valid command."); - } - } + await yargs(Deno.args) + .scriptName("mail-relay") + .command({ + command: "sendmail", + describe: "send mail via this server's endpoint", + handler: async (_argv) => { + const { config } = createBaseServices(); + await sendMail(config.getInt("httpPort")); + }, + }) + .command({ + command: "live", + describe: "work with live mails", + builder: (builder) => { + return builder + .command({ + command: "list", + describe: "list live mails", + handler: listLives, + }) + .command({ + command: "recycle", + describe: "recycle all live mails", + handler: recycleLives, + }) + .demandCommand(1, "One command must be specified."); + }, + handler: () => {}, + }) + .command({ + command: "serve", + describe: "start the http and smtp servers", + builder: (builder) => builder.option("real", { type: "boolean" }), + handler: (argv) => serve(argv.real), + }) + .demandCommand(1, "One command must be specified.") + .help() + .strict() + .parse(); } diff --git a/deno/mail-relay/aws/deliver.ts b/deno/mail-relay/aws/deliver.ts index 3e1f162..4dd4b3a 100644 --- a/deno/mail-relay/aws/deliver.ts +++ b/deno/mail-relay/aws/deliver.ts @@ -1,12 +1,9 @@ -// spellchecker:words sesv2 amazonses - import { SendEmailCommand, SESv2Client, SESv2ClientConfig, } from "@aws-sdk/client-sesv2"; -import { Logger } from "@crupest/base/log"; import { Mail, MailDeliverContext, SyncMailDeliverer } from "../mail.ts"; declare module "../mail.ts" { @@ -17,13 +14,11 @@ declare module "../mail.ts" { export class AwsMailDeliverer extends SyncMailDeliverer { readonly name = "aws"; - readonly #logger; readonly #aws; readonly #ses; - constructor(logger: Logger, aws: SESv2ClientConfig) { - super(logger); - this.#logger = logger; + constructor(aws: SESv2ClientConfig) { + super(); this.#aws = aws; this.#ses = new SESv2Client(aws); } @@ -32,7 +27,7 @@ export class AwsMailDeliverer extends SyncMailDeliverer { mail: Mail, context: MailDeliverContext, ): Promise<void> { - this.#logger.info("Begin to call aws send-email api..."); + console.info("Begin to call aws send-email api..."); try { const sendCommand = new SendEmailCommand({ @@ -43,7 +38,7 @@ export class AwsMailDeliverer extends SyncMailDeliverer { const res = await this.#ses.send(sendCommand); if (res.MessageId == null) { - this.#logger.warn("Aws send-email returns no message id."); + console.warn("Aws send-email returns no message id."); } else { context.result.awsMessageId = `${res.MessageId}@${this.#aws.region}.amazonses.com`; diff --git a/deno/mail-relay/aws/fetch.ts b/deno/mail-relay/aws/fetch.ts index ef1ba5f..9278e63 100644 --- a/deno/mail-relay/aws/fetch.ts +++ b/deno/mail-relay/aws/fetch.ts @@ -7,8 +7,7 @@ import { S3ClientConfig, } from "@aws-sdk/client-s3"; -import { toFileNameString } from "@crupest/base/date"; -import { Logger } from "@crupest/base/log"; +import { toFileNameString } from "@crupest/base"; import { Mail } from "../mail.ts"; @@ -42,18 +41,16 @@ export type AwsS3MailConsumer = ( export class AwsMailFetcher { readonly #livePrefix = "mail/live/"; readonly #archivePrefix = "mail/archive/"; - readonly #logger; readonly #s3; readonly #bucket; - constructor(logger: Logger, aws: S3ClientConfig, bucket: string) { - this.#logger = logger; + constructor(aws: S3ClientConfig, bucket: string) { this.#s3 = new S3Client(aws); this.#bucket = bucket; } async listLiveMails(): Promise<string[]> { - this.#logger.info("Begin to retrieve live mails."); + console.info("Begin to retrieve live mails."); const listCommand = new ListObjectsV2Command({ Bucket: this.#bucket, @@ -62,16 +59,14 @@ export class AwsMailFetcher { const res = await this.#s3.send(listCommand); if (res.Contents == null) { - this.#logger.warn("Listing live mails in S3 returns null Content."); + console.warn("Listing live mails in S3 returns null Content."); return []; } const result: string[] = []; for (const object of res.Contents) { if (object.Key == null) { - this.#logger.warn( - "Listing live mails in S3 returns an object with no Key.", - ); + console.warn("Listing live mails in S3 returns an object with no Key."); continue; } @@ -83,9 +78,9 @@ export class AwsMailFetcher { } async consumeS3Mail(s3Key: string, consumer: AwsS3MailConsumer) { - this.#logger.info(`Begin to consume s3 mail ${s3Key} ...`); + console.info(`Begin to consume s3 mail ${s3Key} ...`); - this.#logger.info(`Fetching s3 mail ${s3Key}...`); + console.info(`Fetching s3 mail ${s3Key}...`); const mailPath = `${this.#livePrefix}${s3Key}`; const command = new GetObjectCommand({ Bucket: this.#bucket, @@ -98,32 +93,33 @@ export class AwsMailFetcher { } const rawMail = await res.Body.transformToString(); - this.#logger.info(`Done fetching s3 mail ${s3Key}.`); + console.info(`Done fetching s3 mail ${s3Key}.`); - this.#logger.info(`Calling consumer...`); + console.info(`Calling consumer...`); await consumer(rawMail, s3Key); - this.#logger.info(`Done consuming s3 mail ${s3Key}.`); + console.info(`Done consuming s3 mail ${s3Key}.`); const date = new Mail(rawMail) - .startSimpleParse(this.#logger) + .startSimpleParse() .sections() .headers() .date(); - const dateString = - date != null ? toFileNameString(date, true) : "invalid-date"; + const dateString = date != null + ? toFileNameString(date, true) + : "invalid-date"; const newPath = `${this.#archivePrefix}${dateString}/${s3Key}`; - this.#logger.info(`Archiving s3 mail ${s3Key} to ${newPath}...`); + console.info(`Archiving s3 mail ${s3Key} to ${newPath}...`); await s3MoveObject(this.#s3, this.#bucket, mailPath, newPath); - this.#logger.info(`Done archiving s3 mail ${s3Key}.`); + console.info(`Done archiving s3 mail ${s3Key}.`); - this.#logger.info(`Done consuming s3 mail ${s3Key}.`); + console.info(`Done consuming s3 mail ${s3Key}.`); } async recycleLiveMails(consumer: AwsS3MailConsumer) { - this.#logger.info("Begin to recycle live mails..."); + console.info("Begin to recycle live mails..."); const mails = await this.listLiveMails(); - this.#logger.info(`Found ${mails.length} live mails`); + console.info(`Found ${mails.length} live mails`); for (const s3Key of mails) { await this.consumeS3Mail(s3Key, consumer); } diff --git a/deno/mail-relay/aws/mail.ts b/deno/mail-relay/aws/mail.ts index d2cfad1..cc05d23 100644 --- a/deno/mail-relay/aws/mail.ts +++ b/deno/mail-relay/aws/mail.ts @@ -8,17 +8,17 @@ export class AwsMailMessageIdRewriteHook implements MailDeliverHook { } async callback(context: MailDeliverContext): Promise<void> { - context.logger.info("Rewrite message ids..."); + console.info("Rewrite message ids..."); const addresses = context.mail.simpleFindAllAddresses(); - context.logger.info(`Addresses found in mail: ${addresses.join(", ")}.`); + console.info(`Addresses found in mail: ${addresses.join(", ")}.`); for (const address of addresses) { const awsMessageId = await this.#lookup(address); if (awsMessageId != null && awsMessageId.length !== 0) { - context.logger.info(`Rewrite ${address} to ${awsMessageId}.`); + console.info(`Rewrite ${address} to ${awsMessageId}.`); context.mail.raw = context.mail.raw.replaceAll(address, awsMessageId); } } - context.logger.info("Done rewrite message ids."); + console.info("Done rewrite message ids."); } } @@ -30,24 +30,20 @@ export class AwsMailMessageIdSaveHook implements MailDeliverHook { } async callback(context: MailDeliverContext): Promise<void> { - context.logger.info("Save aws message ids..."); + console.info("Save aws message ids..."); const messageId = context.mail - .startSimpleParse(context.logger) + .startSimpleParse() .sections() .headers() .messageId(); if (messageId == null) { - context.logger.info( - "Original mail does not have message id. Skip saving.", - ); + console.info("Original mail does not have message id. Skip saving."); return; } if (context.result.awsMessageId != null) { - context.logger.info( - `Saving ${messageId} => ${context.result.awsMessageId}.`, - ); + console.info(`Saving ${messageId} => ${context.result.awsMessageId}.`); await this.#record(messageId, context.result.awsMessageId); } - context.logger.info("Done save message ids."); + console.info("Done save message ids."); } } |