diff options
| author | Yuqian Yang <crupest@crupest.life> | 2025-04-30 00:20:23 +0800 | 
|---|---|---|
| committer | Yuqian Yang <crupest@crupest.life> | 2025-05-14 17:14:29 +0800 | 
| commit | 8930bb11fce19c773201b4f499d99c1c3c28efa6 (patch) | |
| tree | 5ad13f3fc5fc7c6bd914aad39e0ffc824cc0d612 /services | |
| parent | 8ba08870fdb3bafa7b8739c4f1c57a70b8780143 (diff) | |
| download | crupest-8930bb11fce19c773201b4f499d99c1c3c28efa6.tar.gz crupest-8930bb11fce19c773201b4f499d99c1c3c28efa6.tar.bz2 crupest-8930bb11fce19c773201b4f499d99c1c3c28efa6.zip | |
HALF WORK!: 2025-5-14 2
Diffstat (limited to 'services')
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/app.ts | 32 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/aws/app.ts | 22 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/aws/retriever.ts (renamed from services/docker/mail-server/aws-sendmail/aws/retriver.ts) | 8 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/aws/service.ts | 48 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/db.ts | 30 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/deno.json | 4 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/deno.lock | 117 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/mail.ts | 11 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/main.ts | 18 | ||||
| -rw-r--r-- | services/docker/mail-server/aws-sendmail/traffic.ts | 21 | 
10 files changed, 215 insertions, 96 deletions
| diff --git a/services/docker/mail-server/aws-sendmail/app.ts b/services/docker/mail-server/aws-sendmail/app.ts new file mode 100644 index 0000000..e3554d7 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/app.ts @@ -0,0 +1,32 @@ +import { Hono } from "@hono/hono"; + +import { Logger, setLogger } from "./logger.ts"; +import { Config, setConfig } from "./config.ts"; +import { DbService } from "./db.ts"; +import { MailDeliverer } from "./mail.ts"; +import { DovecotMailDeliverer } from "./dovecot.ts"; + +export abstract class AppBase { +  protected readonly db: DbService; +  protected readonly localDeliverer: MailDeliverer; + +  constructor() { +    setLogger(new Logger("log")); +    setConfig(new Config()); + +    this.db = new DbService(); +    this.localDeliverer = new DovecotMailDeliverer(); +  } + +  protected abstract get outboundMailDeliverer(): MailDeliverer; +  protected setupHono(_hono: Hono): Promise<void> { +    return Promise.resolve(); +  } + +  async run(): Promise<void> { +    const hono = new Hono(); +    await this.setupHono(hono); + +    Deno.serve(hono.fetch); +  } +} diff --git a/services/docker/mail-server/aws-sendmail/aws/app.ts b/services/docker/mail-server/aws-sendmail/aws/app.ts new file mode 100644 index 0000000..e25c92c --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/aws/app.ts @@ -0,0 +1,22 @@ +import { Hono } from "https://jsr.io/@hono/hono/4.7.9/src/hono.ts"; +import { AppBase } from "../app.ts"; +import { MailDeliverer } from "../mail.ts"; +import { AwsContext } from "./context.ts"; +import { AwsMailDeliverer } from "./deliver.ts"; + +export class AwsRelayApp extends AppBase { +  private readonly context = new AwsContext(); +  private readonly deliverer = new AwsMailDeliverer(this.context); + +  constructor() { +    super(); +  } + +  protected override setupHono(_hono: Hono): Promise<void> { + +  } + +  protected override get outboundMailDeliverer(): MailDeliverer { +    return this.deliverer; +  } +} diff --git a/services/docker/mail-server/aws-sendmail/aws/retriver.ts b/services/docker/mail-server/aws-sendmail/aws/retriever.ts index b599c15..de577b0 100644 --- a/services/docker/mail-server/aws-sendmail/aws/retriver.ts +++ b/services/docker/mail-server/aws-sendmail/aws/retriever.ts @@ -17,7 +17,6 @@ export class AwsMailRetriever {    readonly mailBucket = getConfig().getValue("awsMailBucket");    private readonly s3Client; -  private readonly liveMailRecyclerAborter = new AbortController();    constructor(      aws: AwsContext, @@ -27,13 +26,6 @@ export class AwsMailRetriever {      this.s3Client = new S3Client({ region, credentials });    } -  setupLiveMailRecycler() { -    Deno.cron("live-mail-recycler", "0 */6 * * *", { -      signal: this.liveMailRecyclerAborter.signal, -    }, () => { -    }); -  } -    async listLiveMails(): Promise<string[]> {      const listCommand = new ListObjectsV2Command({        Bucket: this.mailBucket, diff --git a/services/docker/mail-server/aws-sendmail/aws/service.ts b/services/docker/mail-server/aws-sendmail/aws/service.ts new file mode 100644 index 0000000..d0db7ae --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/aws/service.ts @@ -0,0 +1,48 @@ +import { Mail } from "../mail.ts"; +import { AwsContext } from "./context.ts"; +import { AwsMailRetriever } from "./retriever.ts"; + +export interface AwsServiceSetupOptions { +  receiveCallback: (mail: Mail) => Promise<void>; +} + +interface Setup { +  receiveCallback: (mail: Mail) => Promise<void>; +  retriever: AwsMailRetriever; +  liveMailRecyclerAborter: AbortController; +} + +export class AwsService implements Disposable { +  private _setup: Setup | null = null; + +  constructor(private readonly aws: AwsContext) {} + +  setup(options: AwsServiceSetupOptions): Disposable { +    if (this._setup != null) { +      // TODO: Better error. +      throw new Error("Aws service has already been set up."); +    } +    const { receiveCallback } = options; +    const liveMailRecyclerAborter = new AbortController(); +    const retriever = new AwsMailRetriever(this.aws, receiveCallback); + +    Deno.cron("live-mail-recycler", "0 */6 * * *", { +      signal: liveMailRecyclerAborter.signal, +    }, () => { +      retriever.recycleLiveMails(); +    }); + +    this._setup = { +      receiveCallback, +      retriever, +      liveMailRecyclerAborter, +    }; + +    return this; +  } + +  [Symbol.dispose]() { +    if (this._setup == null) return; +    this._setup.liveMailRecyclerAborter.abort("Aws service is being disposed."); +  } +} diff --git a/services/docker/mail-server/aws-sendmail/db.ts b/services/docker/mail-server/aws-sendmail/db.ts index e239e72..e5307be 100644 --- a/services/docker/mail-server/aws-sendmail/db.ts +++ b/services/docker/mail-server/aws-sendmail/db.ts @@ -3,6 +3,11 @@  import { SQLocalKysely } from "sqlocal/kysely";  import { Generated, Insertable, Kysely, Migration, Migrator } from "kysely"; +import { Mail } from "./mail.ts"; + +export class DbError extends Error { +} +  const tableNames = {    mail: {      table: "mail", @@ -10,6 +15,7 @@ const tableNames = {        id: "id",        messageId: "message_id",        awsMessageId: "aws_message_id", +      date: "date",        raw: "raw",      },    }, @@ -19,6 +25,7 @@ interface MailTable {    [tableNames.mail.columns.id]: Generated<number>;    [tableNames.mail.columns.messageId]: string;    [tableNames.mail.columns.awsMessageId]: string | null; +  [tableNames.mail.columns.date]: number | null;    [tableNames.mail.columns.raw]: string;  } @@ -45,6 +52,7 @@ const migrations: Record<string, Migration> = {            (col) => col.notNull().unique(),          )          .addColumn(names.columns.awsMessageId, "text", (col) => col.unique()) +        .addColumn(names.columns.date, "integer")          .addColumn(names.columns.raw, "text", (col) => col.notNull())          .execute(); @@ -96,7 +104,27 @@ export class DbService {      await this._migrator.migrateToLatest();    } -  async addMail(mail: Insertable<MailTable>): Promise<void> { +  async addMail(mail: Insertable<MailTable> | Mail, options?: { +    allowNullAwsMessageId?: boolean; +  }): Promise<void> { +    if (mail instanceof Mail) { +      if (mail.messageId == null) { +        throw new DbError("Mail object has no message id."); +      } +      mail = { +        message_id: mail.messageId, +        aws_message_id: mail.awsMessageId, +        date: mail.simpleGetDate()?.getTime(), +        raw: mail.raw, +      }; +    } +    if ( +      mail.aws_message_id == null && +      !(options?.allowNullAwsMessageId === true) +    ) { +      throw new DbError("Aws message id is missing but it is required."); +    } +      await this._db.insertInto(tableNames.mail.table).values(mail)        .executeTakeFirstOrThrow();    } diff --git a/services/docker/mail-server/aws-sendmail/deno.json b/services/docker/mail-server/aws-sendmail/deno.json index 67dd7d1..74cbf9c 100644 --- a/services/docker/mail-server/aws-sendmail/deno.json +++ b/services/docker/mail-server/aws-sendmail/deno.json @@ -6,10 +6,12 @@    "imports": {      "@aws-sdk/client-s3": "npm:@aws-sdk/client-s3@^3.797.0",      "@aws-sdk/client-sesv2": "npm:@aws-sdk/client-sesv2@^3.782.0", -    "@oak/oak": "jsr:@oak/oak@^17.1.4", +    "@hono/hono": "jsr:@hono/hono@^4.7.9",      "@std/cli": "jsr:@std/cli@^1.0.17",      "@std/encoding": "jsr:@std/encoding@^1.0.10", +    "@std/expect": "jsr:@std/expect@^1.0.16",      "@std/path": "jsr:@std/path@^1.0.9", +    "@std/testing": "jsr:@std/testing@^1.0.12",      "kysely": "npm:kysely@^0.28.2",      "sqlocal": "npm:sqlocal@^0.14.0"    } diff --git a/services/docker/mail-server/aws-sendmail/deno.lock b/services/docker/mail-server/aws-sendmail/deno.lock index 81e20d7..3f53c68 100644 --- a/services/docker/mail-server/aws-sendmail/deno.lock +++ b/services/docker/mail-server/aws-sendmail/deno.lock @@ -1,75 +1,78 @@  { -  "version": "4", +  "version": "5",    "specifiers": { -    "jsr:@oak/commons@1": "1.0.0", -    "jsr:@oak/oak@^17.1.4": "17.1.4", +    "jsr:@hono/hono@^4.7.9": "4.7.9",      "jsr:@std/assert@1": "1.0.13", -    "jsr:@std/bytes@1": "1.0.5", +    "jsr:@std/assert@^1.0.13": "1.0.13", +    "jsr:@std/async@^1.0.13": "1.0.13",      "jsr:@std/cli@^1.0.17": "1.0.17", -    "jsr:@std/crypto@1": "1.0.4", +    "jsr:@std/data-structures@^1.0.8": "1.0.8",      "jsr:@std/encoding@1": "1.0.10",      "jsr:@std/encoding@^1.0.10": "1.0.10", -    "jsr:@std/http@1": "1.0.15", -    "jsr:@std/media-types@1": "1.1.0", +    "jsr:@std/expect@^1.0.16": "1.0.16", +    "jsr:@std/fs@^1.0.17": "1.0.17", +    "jsr:@std/internal@^1.0.6": "1.0.7", +    "jsr:@std/internal@^1.0.7": "1.0.7",      "jsr:@std/path@1": "1.0.9",      "jsr:@std/path@^1.0.9": "1.0.9", +    "jsr:@std/testing@^1.0.12": "1.0.12",      "npm:@aws-sdk/client-s3@^3.797.0": "3.797.0",      "npm:@aws-sdk/client-sesv2@^3.782.0": "3.782.0",      "npm:@types/node@*": "22.12.0",      "npm:kysely@~0.28.2": "0.28.2", -    "npm:path-to-regexp@^6.3.0": "6.3.0",      "npm:sqlocal@0.14": "0.14.0_kysely@0.28.2"    },    "jsr": { -    "@oak/commons@1.0.0": { -      "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac", -      "dependencies": [ -        "jsr:@std/assert", -        "jsr:@std/bytes", -        "jsr:@std/crypto", -        "jsr:@std/encoding@1", -        "jsr:@std/http", -        "jsr:@std/media-types" -      ] +    "@hono/hono@4.7.9": { +      "integrity": "929baffb76209d2ce4c3442bab45957d5dda1726efb82cef13ec3a51d239c543"      }, -    "@oak/oak@17.1.4": { -      "integrity": "60530b582bf276ff741e39cc664026781aa08dd5f2bc5134d756cc427bf2c13e", +    "@std/assert@1.0.13": { +      "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",        "dependencies": [ -        "jsr:@oak/commons", -        "jsr:@std/assert", -        "jsr:@std/bytes", -        "jsr:@std/http", -        "jsr:@std/media-types", -        "jsr:@std/path@1", -        "npm:path-to-regexp" +        "jsr:@std/internal@^1.0.6"        ]      }, -    "@std/assert@1.0.13": { -      "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29" -    }, -    "@std/bytes@1.0.5": { -      "integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e" +    "@std/async@1.0.13": { +      "integrity": "1d76ca5d324aef249908f7f7fe0d39aaf53198e5420604a59ab5c035adc97c96"      },      "@std/cli@1.0.17": {        "integrity": "e15b9abe629e17be90cc6216327f03a29eae613365f1353837fa749aad29ce7b"      }, -    "@std/crypto@1.0.4": { -      "integrity": "cee245c453bd5366207f4d8aa25ea3e9c86cecad2be3fefcaa6cb17203d79340" +    "@std/data-structures@1.0.8": { +      "integrity": "2fb7219247e044c8fcd51341788547575653c82ae2c759ff209e0263ba7d9b66"      },      "@std/encoding@1.0.10": {        "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"      }, -    "@std/http@1.0.15": { -      "integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159", +    "@std/expect@1.0.16": { +      "integrity": "ceeef6dda21f256a5f0f083fcc0eaca175428b523359a9b1d9b3a1df11cc7391", +      "dependencies": [ +        "jsr:@std/assert@^1.0.13", +        "jsr:@std/internal@^1.0.7" +      ] +    }, +    "@std/fs@1.0.17": { +      "integrity": "1c00c632677c1158988ef7a004cb16137f870aafdb8163b9dce86ec652f3952b",        "dependencies": [ -        "jsr:@std/encoding@^1.0.10" +        "jsr:@std/path@^1.0.9"        ]      }, -    "@std/media-types@1.1.0": { -      "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" +    "@std/internal@1.0.7": { +      "integrity": "39eeb5265190a7bc5d5591c9ff019490bd1f2c3907c044a11b0d545796158a0f"      },      "@std/path@1.0.9": {        "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" +    }, +    "@std/testing@1.0.12": { +      "integrity": "fec973a45ccc62c540fb89296199051fee142409138fd6e3eae409366bcd4720", +      "dependencies": [ +        "jsr:@std/assert@^1.0.13", +        "jsr:@std/async", +        "jsr:@std/data-structures", +        "jsr:@std/fs", +        "jsr:@std/internal@^1.0.7", +        "jsr:@std/path@^1.0.9" +      ]      }    },    "npm": { @@ -886,6 +889,9 @@          "@smithy/node-config-provider",          "@smithy/types",          "tslib" +      ], +      "optionalPeers": [ +        "aws-crt@>=1.0.0"        ]      },      "@aws-sdk/util-user-agent-node@3.796.0": { @@ -896,6 +902,9 @@          "@smithy/node-config-provider",          "@smithy/types",          "tslib" +      ], +      "optionalPeers": [ +        "aws-crt@>=1.0.0"        ]      },      "@aws-sdk/xml-builder@3.775.0": { @@ -1356,7 +1365,8 @@        ]      },      "@sqlite.org/sqlite-wasm@3.48.0-build4": { -      "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==" +      "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==", +      "bin": true      },      "@types/node@22.12.0": {        "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", @@ -1379,7 +1389,9 @@          "@ungap/structured-clone",          "@ungap/with-resolvers",          "gc-hook", -        "proxy-target", +        "proxy-target" +      ], +      "optionalDependencies": [          "ws"        ]      }, @@ -1387,7 +1399,8 @@        "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==",        "dependencies": [          "strnum" -      ] +      ], +      "bin": true      },      "gc-hook@0.3.1": {        "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" @@ -1395,9 +1408,6 @@      "kysely@0.28.2": {        "integrity": "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A=="      }, -    "path-to-regexp@6.3.0": { -      "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" -    },      "proxy-target@3.0.2": {        "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ=="      }, @@ -1407,6 +1417,10 @@          "@sqlite.org/sqlite-wasm",          "coincident",          "kysely" +      ], +      "optionalPeers": [ +        "drizzle-orm@*", +        "kysely"        ]      },      "strnum@1.1.2": { @@ -1419,18 +1433,25 @@        "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="      },      "uuid@9.0.1": { -      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" +      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", +      "bin": true      },      "ws@8.18.1": { -      "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==" +      "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", +      "optionalPeers": [ +        "bufferutil@^4.0.1", +        "utf-8-validate@>=5.0.2" +      ]      }    },    "workspace": {      "dependencies": [ -      "jsr:@oak/oak@^17.1.4", +      "jsr:@hono/hono@^4.7.9",        "jsr:@std/cli@^1.0.17",        "jsr:@std/encoding@^1.0.10", +      "jsr:@std/expect@^1.0.16",        "jsr:@std/path@^1.0.9", +      "jsr:@std/testing@^1.0.12",        "npm:@aws-sdk/client-s3@^3.797.0",        "npm:@aws-sdk/client-sesv2@^3.782.0",        "npm:kysely@~0.28.2", diff --git a/services/docker/mail-server/aws-sendmail/mail.ts b/services/docker/mail-server/aws-sendmail/mail.ts index 98afcaa..b974e49 100644 --- a/services/docker/mail-server/aws-sendmail/mail.ts +++ b/services/docker/mail-server/aws-sendmail/mail.ts @@ -163,14 +163,21 @@ export class Mail {      return headers;    } -  simpleGetDate(): Date | null { +  /** +   * Find the Date header and parse it to date. +   * @param invalidToNull Whether to convert invalid date to null. Default is false. +   * @returns Parsed date (may be an invalid date if `invalidToNull` us false). +   * `null` if the date header is missing or parsing fails and `invalidToNull` +   * is true. +   */ +  simpleGetDate<T = null>(invalidValue: T | null = null): Date | T | null {      const headers = this.simpleParseHeaders();      for (const [key, value] of headers) {        if (key.toLowerCase() === "date") {          const date = new Date(value);          if (isNaN(date.getTime())) {            getLogger().warn(`Invalid date string (${value}) found in header.`); -          return null; +          return invalidValue;          }          return date;        } diff --git a/services/docker/mail-server/aws-sendmail/main.ts b/services/docker/mail-server/aws-sendmail/main.ts index dcc2af1..490d990 100644 --- a/services/docker/mail-server/aws-sendmail/main.ts +++ b/services/docker/mail-server/aws-sendmail/main.ts @@ -1,18 +1,6 @@ -import { AwsContext } from "./aws/context.ts"; -import { MailTrafficHandler } from "./traffic.ts"; -import { Logger, setLogger } from "./logger.ts"; -import { Config, setConfig } from "./config.ts"; - -class App { -  readonly aws = new AwsContext(); -  readonly mailTrafficDeliverer = new MailTrafficHandler(this.aws); - -  constructor() { -    setLogger(new Logger("log")); -    setConfig(new Config()); -  } -} +import { AwsRelayApp } from "./aws/app.ts";  if (import.meta.main) { -  const app = new App(); +  const app = new AwsRelayApp(); +  await app.run();  } diff --git a/services/docker/mail-server/aws-sendmail/traffic.ts b/services/docker/mail-server/aws-sendmail/traffic.ts deleted file mode 100644 index 7ecc405..0000000 --- a/services/docker/mail-server/aws-sendmail/traffic.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MailDeliverer } from "./mail.ts"; -import { DbService } from "./db.ts"; -import { DovecotMailDeliverer } from "./dovecot.ts"; -import { AwsContext } from "./aws/context.ts"; -import { AwsMailDeliverer } from "./aws/deliver.ts"; - -export abstract class MailTrafficHandler { -  constructor( -    public readonly receiver: MailDeliverer, -    public readonly sender: MailDeliverer, -  ) {} -} - -export class AwsRelayTrafficHandler extends MailTrafficHandler { -  constructor( -    private readonly db: DbService, -    private readonly aws: AwsContext, -  ) { -    super(new DovecotMailDeliverer(), new AwsMailDeliverer(aws)); -  } -} | 
