diff options
| author | Yuqian Yang <crupest@crupest.life> | 2025-06-16 01:53:32 +0800 | 
|---|---|---|
| committer | Yuqian Yang <crupest@crupest.life> | 2025-06-17 21:37:43 +0800 | 
| commit | f160809fabe9241867c7e3831351201f3df6c768 (patch) | |
| tree | 01eee9001c8be821a6a6fdc6697e042985bdee04 /deno/mail-relay | |
| parent | ee722b324a7918d79da02e9f54f57d3a416a396a (diff) | |
| download | crupest-f160809fabe9241867c7e3831351201f3df6c768.tar.gz crupest-f160809fabe9241867c7e3831351201f3df6c768.tar.bz2 crupest-f160809fabe9241867c7e3831351201f3df6c768.zip | |
mail: add save aws message id mail.
Diffstat (limited to 'deno/mail-relay')
| -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 | 
5 files changed, 126 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"]; | 
