diff options
author | Yuqian Yang <crupest@crupest.life> | 2025-07-01 17:29:07 +0800 |
---|---|---|
committer | Yuqian Yang <crupest@crupest.life> | 2025-07-01 17:29:07 +0800 |
commit | f4d60fb87f9c6d86a584eba11e6a3d17479af201 (patch) | |
tree | 37f1a7577e67acc06ddcfc918affb1ecd0df6038 | |
parent | 5e250ddcdbd6307989071e4ea794b1ad39e928e5 (diff) | |
download | crupest-f4d60fb87f9c6d86a584eba11e6a3d17479af201.tar.gz crupest-f4d60fb87f9c6d86a584eba11e6a3d17479af201.tar.bz2 crupest-f4d60fb87f9c6d86a584eba11e6a3d17479af201.zip |
mail: remove 'aws' of aws message id map.
-rw-r--r-- | deno/base/lib.ts | 2 | ||||
-rw-r--r-- | deno/mail/app.ts | 2 | ||||
-rw-r--r-- | deno/mail/aws/app.ts | 17 | ||||
-rw-r--r-- | deno/mail/aws/deliver.ts | 12 | ||||
-rw-r--r-- | deno/mail/aws/mail.ts | 59 | ||||
-rw-r--r-- | deno/mail/db.test.ts | 8 | ||||
-rw-r--r-- | deno/mail/db.ts | 36 | ||||
-rw-r--r-- | deno/mail/dumb-smtp-server.ts | 4 | ||||
-rw-r--r-- | deno/mail/mail.ts | 76 |
9 files changed, 109 insertions, 107 deletions
diff --git a/deno/base/lib.ts b/deno/base/lib.ts index 3c69e0a..af75115 100644 --- a/deno/base/lib.ts +++ b/deno/base/lib.ts @@ -3,7 +3,7 @@ function camelCaseToKebabCase(str: string): string { } function prependNonEmpty<T>( - object: T, + object: T | null | undefined, prefix: string = " ", ): string { if (object == null) return ""; diff --git a/deno/mail/app.ts b/deno/mail/app.ts index 332c430..2a8c78a 100644 --- a/deno/mail/app.ts +++ b/deno/mail/app.ts @@ -49,7 +49,7 @@ export function createHono(outbound: MailDeliverer, inbound: MailDeliverer) { } else { const result = await outbound.deliverRaw(body); return context.json({ - awsMessageId: result.awsMessageId, + newMessageId: result.newMessageId, }); } }); diff --git a/deno/mail/aws/app.ts b/deno/mail/aws/app.ts index 6c7ede7..7e16488 100644 --- a/deno/mail/aws/app.ts +++ b/deno/mail/aws/app.ts @@ -13,14 +13,10 @@ import { DbService } from "../db.ts"; import { createHono, createInbound, createSmtp, sendMail } from "../app.ts"; import { DovecotMailDeliverer } from "../dovecot.ts"; import { MailDeliverer } from "../mail.ts"; -import { - AwsMailMessageIdRewriteHook, - AwsMailMessageIdSaveHook, -} from "./mail.ts"; +import { MessageIdRewriteHook, MessageIdSaveHook } from "../mail.ts"; import { AwsMailDeliverer } from "./deliver.ts"; import { AwsMailFetcher, LiveMailNotFoundError } from "./fetch.ts"; - const PREFIX = "crupest-mail-server"; const CONFIG_DEFINITIONS = { dataPath: { @@ -105,12 +101,12 @@ function createOutbound( ) { const deliverer = new AwsMailDeliverer(awsOptions); deliverer.preHooks.push( - new AwsMailMessageIdRewriteHook(db.messageIdToAws.bind(db)), + new MessageIdRewriteHook(db.messageIdToNew.bind(db)), ); deliverer.postHooks.push( - new AwsMailMessageIdSaveHook( - async (original, aws, context) => { - await db.addMessageIdMap({ message_id: original, aws_message_id: aws }); + new MessageIdSaveHook( + async (original, new_message_id, context) => { + await db.addMessageIdMap({ message_id: original, new_message_id }); void local?.saveNewSent(context.logTag, context.mail, original); }, ), @@ -240,7 +236,8 @@ function createServerServices() { } async function serve(cron: boolean = false) { - const { config, fetcher, inbound, smtp, dbService, hono } = createServerServices(); + const { config, fetcher, inbound, smtp, dbService, hono } = + createServerServices(); await dbService.migrate(); diff --git a/deno/mail/aws/deliver.ts b/deno/mail/aws/deliver.ts index 0195369..37a871d 100644 --- a/deno/mail/aws/deliver.ts +++ b/deno/mail/aws/deliver.ts @@ -6,12 +6,6 @@ import { import { Mail, MailDeliverContext, MailDeliverer } from "../mail.ts"; -declare module "../mail.ts" { - interface MailDeliverResult { - awsMessageId?: string; - } -} - export class AwsMailDeliverer extends MailDeliverer { readonly name = "aws"; readonly #aws; @@ -42,12 +36,12 @@ export class AwsMailDeliverer extends MailDeliverer { "AWS send-email returned null message id.", ); } else { - context.result.awsMessageId = + context.result.newMessageId = `${res.MessageId}@${this.#aws.region}.amazonses.com`; } - context.result.smtpMessage = - `AWS Message ID: ${context.result.awsMessageId}`; + context.result.messageForSmtp = + `AWS Message ID: ${context.result.newMessageId}`; context.result.recipients.set("*", { kind: "success", message: `Succeeded to call aws send-email api.`, diff --git a/deno/mail/aws/mail.ts b/deno/mail/aws/mail.ts deleted file mode 100644 index 26f3ea0..0000000 --- a/deno/mail/aws/mail.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { MailDeliverContext, MailDeliverHook } from "../mail.ts"; - -export class AwsMailMessageIdRewriteHook implements MailDeliverHook { - readonly #lookup; - - constructor(lookup: (origin: string) => Promise<string | null>) { - this.#lookup = lookup; - } - - async callback(context: MailDeliverContext): Promise<void> { - const addresses = context.mail.simpleFindAllAddresses(); - for (const address of addresses) { - const awsMessageId = await this.#lookup(address); - if (awsMessageId != null && awsMessageId.length !== 0) { - console.info( - context.logTag, - `Rewrite address-line string in mail: ${address} => ${awsMessageId}.`, - ); - context.mail.raw = context.mail.raw.replaceAll(address, awsMessageId); - } - } - } -} - -export class AwsMailMessageIdSaveHook implements MailDeliverHook { - readonly #record; - - constructor( - record: ( - original: string, - aws: string, - context: MailDeliverContext, - ) => Promise<void>, - ) { - this.#record = record; - } - - async callback(context: MailDeliverContext): Promise<void> { - const { messageId } = context.mail.parsed; - if (messageId == null) { - console.warn( - context.logTag, - "Original mail doesn't have message id, skip saving message id map.", - ); - return; - } - if (context.result.awsMessageId != null) { - console.info( - context.logTag, - `Save message id map: ${messageId} => ${context.result.awsMessageId}.`, - ); - context.mail.raw = context.mail.raw.replaceAll( - messageId, - context.result.awsMessageId, - ); - await this.#record(messageId, context.result.awsMessageId, context); - } - } -} diff --git a/deno/mail/db.test.ts b/deno/mail/db.test.ts index 60035c4..8a9ad27 100644 --- a/deno/mail/db.test.ts +++ b/deno/mail/db.test.ts @@ -6,17 +6,17 @@ import { DbService } from "./db.ts"; describe("DbService", () => { const mockRow = { message_id: "mock-message-id@mock.mock", - aws_message_id: "mock-aws-message-id@mock.mock", + new_message_id: "mock-new-message-id@mock.mock", }; it("works", async () => { const db = new DbService(":memory:"); await db.migrate(); await db.addMessageIdMap(mockRow); - expect(await db.messageIdToAws(mockRow.message_id)).toBe( - mockRow.aws_message_id, + expect(await db.messageIdToNew(mockRow.message_id)).toBe( + mockRow.new_message_id, ); - expect(await db.messageIdFromAws(mockRow.aws_message_id)).toBe( + expect(await db.messageIdFromNew(mockRow.new_message_id)).toBe( mockRow.message_id, ); }); diff --git a/deno/mail/db.ts b/deno/mail/db.ts index 062700b..e41f762 100644 --- a/deno/mail/db.ts +++ b/deno/mail/db.ts @@ -53,14 +53,14 @@ class SqliteDatabaseAdapter implements SqliteDatabase { export class DbError extends Error {} -interface AwsMessageIdMapTable { +interface MessageIdMapTable { id: Generated<number>; message_id: string; - aws_message_id: string; + new_message_id: string; } interface Database { - aws_message_id_map: AwsMessageIdMapTable; + message_id_map: MessageIdMapTable; } const migrations: Record<string, Migration> = { @@ -68,16 +68,16 @@ const migrations: Record<string, Migration> = { // deno-lint-ignore no-explicit-any async up(db: Kysely<any>): Promise<void> { await db.schema - .createTable("aws_message_id_map") + .createTable("message_id_map") .addColumn("id", "integer", (col) => col.primaryKey().autoIncrement()) .addColumn("message_id", "text", (col) => col.notNull().unique()) - .addColumn("aws_message_id", "text", (col) => col.notNull().unique()) + .addColumn("new_message_id", "text", (col) => col.notNull().unique()) .execute(); - for (const column of ["message_id", "aws_message_id"]) { + for (const column of ["message_id", "new_message_id"]) { await db.schema - .createIndex(`aws_message_id_map_${column}`) - .on("aws_message_id_map") + .createIndex(`message_id_map_${column}`) + .on("message_id_map") .column(column) .execute(); } @@ -85,7 +85,7 @@ const migrations: Record<string, Migration> = { // deno-lint-ignore no-explicit-any async down(db: Kysely<any>): Promise<void> { - await db.schema.dropTable("aws_message_id_map").execute(); + await db.schema.dropTable("message_id_map").execute(); }, }, }; @@ -117,28 +117,28 @@ export class DbService { } async addMessageIdMap( - mail: Insertable<AwsMessageIdMapTable>, + mail: Insertable<MessageIdMapTable>, ): Promise<number> { const inserted = await this.#kysely - .insertInto("aws_message_id_map") + .insertInto("message_id_map") .values(mail) .executeTakeFirstOrThrow(); return Number(inserted.insertId!); } - async messageIdToAws(messageId: string): Promise<string | null> { + async messageIdToNew(messageId: string): Promise<string | null> { const row = await this.#kysely - .selectFrom("aws_message_id_map") + .selectFrom("message_id_map") .where("message_id", "=", messageId) - .select("aws_message_id") + .select("new_message_id") .executeTakeFirst(); - return row?.aws_message_id ?? null; + return row?.new_message_id ?? null; } - async messageIdFromAws(awsMessageId: string): Promise<string | null> { + async messageIdFromNew(newMessageId: string): Promise<string | null> { const row = await this.#kysely - .selectFrom("aws_message_id_map") - .where("aws_message_id", "=", awsMessageId) + .selectFrom("message_id_map") + .where("new_message_id", "=", newMessageId) .select("message_id") .executeTakeFirst(); return row?.message_id ?? null; diff --git a/deno/mail/dumb-smtp-server.ts b/deno/mail/dumb-smtp-server.ts index 70d5ec0..c3ebf5d 100644 --- a/deno/mail/dumb-smtp-server.ts +++ b/deno/mail/dumb-smtp-server.ts @@ -91,8 +91,8 @@ export class DumbSmtpServer { if (line === ".") { try { console.info(logTag, "Mail data received, begin to relay..."); - const { smtpMessage } = await this.#deliverer.deliverRaw(rawMail); - await send(`250 2.6.0 ${smtpMessage}`); + const result = await this.#deliverer.deliverRaw(rawMail); + await send(`250 2.6.0 ${result.generateMessageForSmtp()}`); rawMail = null; } catch (err) { console.error(logTag, "Relay failed.", err); diff --git a/deno/mail/mail.ts b/deno/mail/mail.ts index 9cc591c..b88ce2b 100644 --- a/deno/mail/mail.ts +++ b/deno/mail/mail.ts @@ -1,5 +1,8 @@ import { encodeBase64 } from "@std/encoding/base64"; import { parse } from "@std/csv/parse"; + +import { StringUtils } from "@crupest/base"; + import { simpleParseMail } from "./mail-parsing.ts"; export class Mail { @@ -47,7 +50,9 @@ export interface MailDeliverRecipientResult { export class MailDeliverResult { message?: string; - smtpMessage?: string; + messageForSmtp?: string; + newMessageId?: string; + recipients = new Map<string, MailDeliverRecipientResult>(); constructor(public mail: Mail) {} @@ -58,8 +63,8 @@ export class MailDeliverResult { generateLogMessage(prefix: string) { const lines = []; if (this.message != null) lines.push(`${prefix} message: ${this.message}`); - if (this.smtpMessage != null) { - lines.push(`${prefix} smtpMessage: ${this.smtpMessage}`); + if (this.messageForSmtp != null) { + lines.push(`${prefix} smtpMessage: ${this.messageForSmtp}`); } for (const [name, result] of this.recipients.entries()) { const { kind, message } = result; @@ -67,6 +72,13 @@ export class MailDeliverResult { } return lines.join("\n"); } + + generateMessageForSmtp(): string { + if (this.messageForSmtp != null) return this.messageForSmtp; + return `2.0.0 OK${ + StringUtils.prependNonEmpty(this.newMessageId) + } Message accepted for delivery`; + } } export class MailDeliverContext { @@ -232,3 +244,61 @@ export class AliasRecipientMailHook implements MailDeliverHook { } } } + +export class MessageIdRewriteHook implements MailDeliverHook { + readonly #lookup; + + constructor(lookup: (origin: string) => Promise<string | null>) { + this.#lookup = lookup; + } + + async callback(context: MailDeliverContext): Promise<void> { + const addresses = context.mail.simpleFindAllAddresses(); + for (const address of addresses) { + const newMessageId = await this.#lookup(address); + if (newMessageId != null && newMessageId.length !== 0) { + console.info( + context.logTag, + `Rewrite address-line string in mail: ${address} => ${newMessageId}.`, + ); + context.mail.raw = context.mail.raw.replaceAll(address, newMessageId); + } + } + } +} + +export class MessageIdSaveHook implements MailDeliverHook { + readonly #record; + + constructor( + record: ( + original: string, + newMessageId: string, + context: MailDeliverContext, + ) => Promise<void>, + ) { + this.#record = record; + } + + async callback(context: MailDeliverContext): Promise<void> { + const { messageId } = context.mail.parsed; + if (messageId == null) { + console.warn( + context.logTag, + "Original mail doesn't have message id, skip saving message id map.", + ); + return; + } + if (context.result.newMessageId != null) { + console.info( + context.logTag, + `Save message id map: ${messageId} => ${context.result.newMessageId}.`, + ); + context.mail.raw = context.mail.raw.replaceAll( + messageId, + context.result.newMessageId, + ); + await this.#record(messageId, context.result.newMessageId, context); + } + } +} |