diff options
-rw-r--r-- | deno/mail-relay/app.ts | 8 | ||||
-rw-r--r-- | deno/mail-relay/aws/app.ts | 19 | ||||
-rw-r--r-- | deno/mail-relay/aws/mail.ts | 14 | ||||
-rw-r--r-- | deno/mail-relay/dovecot.ts | 85 | ||||
-rw-r--r-- | deno/mail-relay/mail.ts | 8 | ||||
-rw-r--r-- | dictionary.txt | 1 |
6 files changed, 127 insertions, 8 deletions
diff --git a/deno/mail-relay/app.ts b/deno/mail-relay/app.ts index eeffc12..8cb33e6 100644 --- a/deno/mail-relay/app.ts +++ b/deno/mail-relay/app.ts @@ -19,14 +19,20 @@ export function createInbound( mailDomain, aliasFile, ldaPath, + doveadmPath, }: { fallback: string[]; mailDomain: string; aliasFile: string; ldaPath: string; + doveadmPath: string; }, ) { - const deliverer = new DovecotMailDeliverer(logFileProvider, ldaPath); + const deliverer = new DovecotMailDeliverer( + logFileProvider, + ldaPath, + doveadmPath, + ); deliverer.preHooks.push( new RecipientFromHeadersHook(mailDomain), new FallbackRecipientHook(new Set(fallback)), diff --git a/deno/mail-relay/aws/app.ts b/deno/mail-relay/aws/app.ts index cb275ae..86f7c6b 100644 --- a/deno/mail-relay/aws/app.ts +++ b/deno/mail-relay/aws/app.ts @@ -19,6 +19,7 @@ import { import { AwsMailDeliverer } from "./deliver.ts"; import { AwsMailFetcher, AwsS3MailConsumer } from "./fetch.ts"; import { createHono, createInbound, createSmtp, sendMail } from "../app.ts"; +import { DovecotMailDeliverer } from "../dovecot.ts"; const PREFIX = "crupest-mail-server"; const CONFIG_DEFINITIONS = { @@ -47,6 +48,10 @@ const CONFIG_DEFINITIONS = { description: "full path of lda executable", default: "/dovecot/libexec/dovecot/dovecot-lda", }, + doveadmPath: { + description: "full path of doveadm executable", + default: "/dovecot/bin/doveadm", + }, inboundFallback: { description: "comma separated addresses used as fallback recipients", default: "", @@ -96,14 +101,18 @@ function createAwsOptions({ function createOutbound( awsOptions: ReturnType<typeof createAwsOptions>, db: DbService, + local?: DovecotMailDeliverer, ) { 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() + new AwsMailMessageIdSaveHook( + async (original, aws, context) => { + await db.addMessageIdMap({ message_id: original, aws_message_id: aws }); + void local?.saveNewSent(original, context.mail); + }, ), ); return deliverer; @@ -182,6 +191,7 @@ function createAwsRecycleOnlyServices() { const inbound = createInbound(logFileProvider, { fallback: config.getList("inboundFallback"), ldaPath: config.get("ldaPath"), + doveadmPath: config.get("doveadmPath"), aliasFile: join(config.get("dataPath"), "aliases.csv"), mailDomain: config.get("mailDomain"), }); @@ -190,12 +200,13 @@ function createAwsRecycleOnlyServices() { return { ...services, inbound, recycler }; } + function createAwsServices() { const services = createAwsRecycleOnlyServices(); - const { config, awsOptions } = services; + const { config, awsOptions, inbound } = services; const dbService = new DbService(join(config.get("dataPath"), "db.sqlite")); - const outbound = createOutbound(awsOptions, dbService); + const outbound = createOutbound(awsOptions, dbService, inbound); return { ...services, dbService, outbound }; } diff --git a/deno/mail-relay/aws/mail.ts b/deno/mail-relay/aws/mail.ts index cc05d23..7ac2332 100644 --- a/deno/mail-relay/aws/mail.ts +++ b/deno/mail-relay/aws/mail.ts @@ -25,7 +25,13 @@ export class AwsMailMessageIdRewriteHook implements MailDeliverHook { export class AwsMailMessageIdSaveHook implements MailDeliverHook { readonly #record; - constructor(record: (original: string, aws: string) => Promise<void>) { + constructor( + record: ( + original: string, + aws: string, + context: MailDeliverContext, + ) => Promise<void>, + ) { this.#record = record; } @@ -42,7 +48,11 @@ export class AwsMailMessageIdSaveHook implements MailDeliverHook { } if (context.result.awsMessageId != null) { console.info(`Saving ${messageId} => ${context.result.awsMessageId}.`); - await this.#record(messageId, context.result.awsMessageId); + context.mail.raw = context.mail.raw.replaceAll( + messageId, + context.result.awsMessageId, + ); + await this.#record(messageId, context.result.awsMessageId, context); } console.info("Done save message ids."); } diff --git a/deno/mail-relay/dovecot.ts b/deno/mail-relay/dovecot.ts index bace225..6d291ee 100644 --- a/deno/mail-relay/dovecot.ts +++ b/deno/mail-relay/dovecot.ts @@ -8,11 +8,17 @@ export class DovecotMailDeliverer extends MailDeliverer { readonly name = "dovecot"; readonly #logFileProvider; readonly #ldaPath; + readonly #doveadmPath; - constructor(logFileProvider: LogFileProvider, ldaPath: string) { + constructor( + logFileProvider: LogFileProvider, + ldaPath: string, + doveadmPath: string, + ) { super(); this.#logFileProvider = logFileProvider; this.#ldaPath = ldaPath; + this.#doveadmPath = doveadmPath; } protected override async doDeliver( @@ -96,4 +102,81 @@ export class DovecotMailDeliverer extends MailDeliverer { console.info("Done handling all recipients."); } + + async #deleteMail( + user: string, + mailbox: string, + messageId: string, + ): Promise<void> { + try { + const args = [ + "expunge", + "-u", + user, + "mailbox", + mailbox, + "header", + "Message-ID", + `<${messageId}>`, + ]; + console.info( + `Run external command ${this.#doveadmPath} ${args.join(" ")} ...`, + ); + const command = new Deno.Command(this.#doveadmPath, { args }); + const status = await command.spawn().status; + if (status.success) { + console.info("Expunged successfully."); + } else { + console.warn("Expunging failed with exit code %d.", status.code); + } + } catch (cause) { + console.warn("Expunging failed with an error thrown: ", cause); + } + } + + async #saveMail(user: string, mailbox: string, mail: Uint8Array) { + try { + const args = ["save", "-u", user, "-m", mailbox]; + console.info( + `Run external command ${this.#doveadmPath} ${args.join(" ")} ...`, + ); + const command = new Deno.Command(this.#doveadmPath, { + args, + stdin: "piped", + }); + const process = command.spawn(); + const stdinWriter = process.stdin.getWriter(); + await stdinWriter.write(mail); + await stdinWriter.close(); + const status = await process.status; + + if (status.success) { + console.info("Saved successfully."); + } else { + console.warn("Saving failed with exit code %d.", status.code); + } + } catch (cause) { + console.warn("Saving failed with an error thrown: ", cause); + } + } + + async saveNewSent(originalMessageId: string, mail: Mail) { + console.info( + "Try to save mail with new id and delete mail with old id in Sent box.", + ); + const from = mail.startSimpleParse().sections().headers() + .from(); + if (from != null) { + console.info("Parsed sender (from): ", from); + await this.#saveMail(from, "Sent", mail.toUtf8Bytes()); + setTimeout(() => { + console.info( + "Try to delete mail in Sent box that has old message id.", + ); + this.#deleteMail(from, "Sent", originalMessageId); + }, 1000 * 15); + } else { + console.warn("Failed to determine from."); + } + } } diff --git a/deno/mail-relay/mail.ts b/deno/mail-relay/mail.ts index d6dfe65..1fa1e6a 100644 --- a/deno/mail-relay/mail.ts +++ b/deno/mail-relay/mail.ts @@ -39,6 +39,14 @@ class MailSimpleParsedHeaders { return date; } + from(): string | undefined { + const fromField = this.getFirst("from"); + if (fromField == null) return undefined; + + const addr = emailAddresses.parseOneAddress(fromField); + return addr?.type === "mailbox" ? addr.address : undefined; + } + recipients(options?: { domain?: string; headers?: string[] }): Set<string> { const domain = options?.domain; const headers = options?.headers ?? ["to", "cc", "bcc", "x-original-to"]; diff --git a/dictionary.txt b/dictionary.txt index e2894d9..dee097e 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -17,6 +17,7 @@ gerrit gohugoio pwsh rclone +doveadm kmod btrfs |