aboutsummaryrefslogtreecommitdiff
path: root/deno/mail-relay/aws
diff options
context:
space:
mode:
Diffstat (limited to 'deno/mail-relay/aws')
-rw-r--r--deno/mail-relay/aws/app.ts157
-rw-r--r--deno/mail-relay/aws/deliver.ts13
-rw-r--r--deno/mail-relay/aws/fetch.ts42
-rw-r--r--deno/mail-relay/aws/mail.ts22
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.");
}
}