aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--deno/mail-relay/app.ts8
-rw-r--r--deno/mail-relay/aws/app.ts19
-rw-r--r--deno/mail-relay/aws/mail.ts14
-rw-r--r--deno/mail-relay/dovecot.ts85
-rw-r--r--deno/mail-relay/mail.ts8
-rw-r--r--dictionary.txt1
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