diff options
Diffstat (limited to 'services/docker')
11 files changed, 150 insertions, 112 deletions
| diff --git a/services/docker/mail-server/aws-sendmail/aws.ts b/services/docker/mail-server/aws-sendmail/aws.ts index e847cf4..cd1c453 100644 --- a/services/docker/mail-server/aws-sendmail/aws.ts +++ b/services/docker/mail-server/aws-sendmail/aws.ts @@ -1,10 +1,8 @@  import { getEnvRequired } from "./base.ts"; -import { Logger } from "./logger.ts";  export class AwsContext { -  constructor(public readonly logger: Logger) {} - -  readonly awsCredentialsProvider = () => { +  readonly region = "ap-southeast-1" +  readonly credentials = () => {      return Promise.resolve(        {          accessKeyId: getEnvRequired("AWS_USER", "aws access key id"), diff --git a/services/docker/mail-server/aws-sendmail/db.ts b/services/docker/mail-server/aws-sendmail/db.ts index 05cc7b5..b7e052f 100644 --- a/services/docker/mail-server/aws-sendmail/db.ts +++ b/services/docker/mail-server/aws-sendmail/db.ts @@ -1,15 +1,9 @@  // spellchecker: words sqlocal kysely insertable updateable  import { SQLocalKysely } from "sqlocal/kysely"; -import { -  Generated, -  Insertable, -  Kysely, -  Migration, -  Migrator, -  Selectable, -  Updateable, -} from "kysely"; +import { Generated, Kysely, Migration, Migrator } from "kysely"; + +import { Mail } from "./mail.ts";  const tableNames = {    mail: { @@ -18,7 +12,7 @@ const tableNames = {        id: "id",        messageId: "message_id",        awsMessageId: "aws_message_id", -      rawMail: "raw_mail", +      raw: "raw",      },    },  } as const; @@ -27,13 +21,9 @@ interface MailTable {    [tableNames.mail.columns.id]: Generated<number>;    [tableNames.mail.columns.messageId]: string;    [tableNames.mail.columns.awsMessageId]: string | null; -  [tableNames.mail.columns.rawMail]: string; +  [tableNames.mail.columns.raw]: string;  } -export type Mail = Selectable<MailTable>; -export type NewMail = Insertable<MailTable>; -export type MailUpdate = Updateable<MailTable>; -  interface Database {    [tableNames.mail.table]: MailTable;  } @@ -57,7 +47,7 @@ const migrations: Record<string, Migration> = {            (col) => col.notNull().unique(),          )          .addColumn(names.columns.awsMessageId, "text", (col) => col.unique()) -        .addColumn(names.columns.rawMail, "text", (col) => col.notNull()) +        .addColumn(names.columns.raw, "text", (col) => col.notNull())          .execute();        for ( @@ -101,8 +91,19 @@ export class DbService {      await this._migrator.migrateToLatest();    } -  async addMail(mail: NewMail): Promise<void> { -    await this._db.insertInto(tableNames.mail.table).values(mail) +  async addMail(mail: Mail): Promise<void> { +    const { raw, message_id, aws_message_id } = mail; +    if (message_id == null) { +      // TODO: Better error. +      throw new Error( +        "Failed to add mail to database. Mail has no message id.", +      ); +    } +    await this._db.insertInto(tableNames.mail.table).values({ +      raw, +      message_id, +      aws_message_id, +    })        .executeTakeFirstOrThrow();    } diff --git a/services/docker/mail-server/aws-sendmail/deliver.ts b/services/docker/mail-server/aws-sendmail/deliver.ts index 7035d8c..e0c6e1c 100644 --- a/services/docker/mail-server/aws-sendmail/deliver.ts +++ b/services/docker/mail-server/aws-sendmail/deliver.ts @@ -1,51 +1,61 @@ +import { Mail } from "./mail.ts"; +  class MailDeliverError extends Error {    constructor(      message: string,      options: ErrorOptions, -    public readonly rawMail: string, +    public readonly mail: Mail,    ) {      super(message, options);    }  } -export class MailDeliverContext { -  constructor(public rawMail: string) {} -} - -type MailDeliverHook<Context> = (context: Context) => Promise<void>; +type MailDeliverHook = (mail: Mail) => Promise<void>; -export abstract class MailDeliverer<out TContext extends MailDeliverContext = MailDeliverContext> { -  preHooks: MailDeliverHook<MailDeliverContext>[] = []; -  postHooks: MailDeliverHook<MailDeliverContext>[] = []; +export abstract class MailDeliverer { +  preHooks: MailDeliverHook[] = []; +  postHooks: MailDeliverHook[] = [];    constructor(public readonly destination: string) {} -  protected abstract doPrepare(rawMail: string): Promise<TContext>; -  protected abstract doDeliver(context: TContext): Promise<void>; +  protected doPrepare(_mail: Mail): Promise<void> { +    return Promise.resolve(); +  } +  protected abstract doDeliver(mail: Mail): Promise<void>; +  protected doFinalize(_mail: Mail): Promise<void> { +    return Promise.resolve(); +  } + +  async deliverRaw(raw: string): Promise<void> { +    const mail = new Mail(raw); +    await this.deliver(mail); +  } -  async deliver(rawMail: string): Promise<void> { -    const context = await this.doPrepare(rawMail); +  async deliver(mail: Mail): Promise<void> { +    this.doPrepare(mail);      for (const hook of this.preHooks) { -      await hook(context); +      await hook(mail);      } -    await this.doDeliver(context); +    await this.doDeliver(mail);      for (const hook of this.postHooks) { -      await hook(context); +      await hook(mail);      } + +    await this.doFinalize(mail);    }    protected throwError(      reason: string, -    rawMail: string, +    mail: Mail,      cause?: unknown,    ): never {      throw new MailDeliverError(        `Failed to deliver mail to ${this.destination}: ${reason}`,        { cause }, -      rawMail, +      mail,      );    }  } diff --git a/services/docker/mail-server/aws-sendmail/delivers/aws.ts b/services/docker/mail-server/aws-sendmail/delivers/aws.ts index 85d86ec..9fe7bec 100644 --- a/services/docker/mail-server/aws-sendmail/delivers/aws.ts +++ b/services/docker/mail-server/aws-sendmail/delivers/aws.ts @@ -1,28 +1,31 @@ -import { SESv2Client } from "@aws-sdk/client-sesv2"; +import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2";  import { AwsContext } from "../aws.ts"; -import { MailDeliverer, MailDeliverContext } from "../deliver.ts"; +import { Mail } from "../mail.ts"; +import { MailDeliverer } from "../deliver.ts"; -export class AwsMailDeliverContext extends MailDeliverContext { -  awsMessageId: string | null = null; - -  constructor(rawMail: string) { -    super(rawMail); -  } -} - -class AwsMailDeliverer extends MailDeliverer<AwsMailDeliverContext> { +export class AwsMailDeliverer extends MailDeliverer {    private _ses;    constructor(readonly aws: AwsContext) {      super("aws"); -    this._ses = new SESv2Client({ credentials: aws.awsCredentialsProvider }); -  } -  protected override doPrepare(rawMail: string): Promise<AwsMailDeliverContext> { -    return Promise.resolve(new AwsMailDeliverContext(rawMail)) +    const { region, credentials } = aws; + +    this._ses = new SESv2Client({ region, credentials });    } -  protected override async doDeliver(context: AwsContext): Promise<void> { +  protected override async doDeliver(mail: Mail): Promise<void> { +    const sendCommand = new SendEmailCommand({ +      Content: { +        Raw: { Data: mail.encodeUtf8() }, +      }, +    }); + +    const res = await this._ses.send(sendCommand); +    if (res.MessageId == null) { +      throw Error("No message id is returned from aws."); +    } +    mail.aws_message_id = res.MessageId;    }  } diff --git a/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts b/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts index e30c558..2b35872 100644 --- a/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts +++ b/services/docker/mail-server/aws-sendmail/delivers/dovecot.ts @@ -1,31 +1,20 @@ -import { Logger } from "../logger.ts"; -import { MailDeliverContext, MailDeliverer } from "../deliver.ts"; +import { getLogger } from "../logger.ts"; +import { MailDeliverer } from "../deliver.ts"; +import { Mail } from "../mail.ts";  export class DovecotMailDeliverer extends MailDeliverer { -  constructor(private readonly logger: Logger) { +  constructor() {      super("dovecot");    }    readonly ldaBin = "dovecot-lda"; -  protected override doPrepare( -    rawMail: string, -  ): Promise<MailDeliverContext> { -    return Promise.resolve(new MailDeliverContext(rawMail)); -  } - -  protected override async doDeliver( -    context: MailDeliverContext, -  ): Promise<void> { -    const { logger, ldaBin } = this; -    const { rawMail } = context; +  protected override async doDeliver(mail: Mail): Promise<void> { +    const { ldaBin } = this;      let status;      try { -      const utf8Encoder = new TextEncoder(); -      // TODO: A problem here is if mail is VERY long, this will block for a long time. -      // Maybe some task queue can be used. -      const utf8Stream = utf8Encoder.encode(rawMail); +      const utf8Stream = mail.encodeUtf8();        const ldaCommand = new Deno.Command(ldaBin, {          stdin: "piped", @@ -33,7 +22,7 @@ export class DovecotMailDeliverer extends MailDeliverer {          stderr: "piped",        });        const ldaProcess = ldaCommand.spawn(); -      logger.logProgramOutput(ldaProcess, ldaBin); +      getLogger().logProgramOutput(ldaProcess, ldaBin);        const stdinWriter = ldaProcess.stdin.getWriter();        await stdinWriter.ready; @@ -42,15 +31,11 @@ export class DovecotMailDeliverer extends MailDeliverer {        status = await ldaProcess.status;      } catch (cause) { -      this.throwError( -        "external error.", -        rawMail, -        cause, -      ); +      this.throwError("external error.", mail, cause);      }      if (!status.success) { -      this.throwError(`${ldaBin} exited with non-zero.`, rawMail); +      this.throwError(`${ldaBin} exited with non-zero.`, mail);      }    }  } diff --git a/services/docker/mail-server/aws-sendmail/delivers/traffic.ts b/services/docker/mail-server/aws-sendmail/delivers/traffic.ts index a3ff52b..3d567f9 100644 --- a/services/docker/mail-server/aws-sendmail/delivers/traffic.ts +++ b/services/docker/mail-server/aws-sendmail/delivers/traffic.ts @@ -1,11 +1,14 @@ -import { Logger } from "../logger.ts";  import { MailDeliverer } from "../deliver.ts";  import { DovecotMailDeliverer } from "./dovecot.ts"; +import { AwsContext } from "../aws.ts"; +import { AwsMailDeliverer } from "./aws.ts";  export class MailTrafficDeliverer {    receiver: MailDeliverer; +  sender: MailDeliverer; -  constructor(logger: Logger) { -    this.receiver = new DovecotMailDeliverer(logger); +  constructor(aws: AwsContext) { +    this.receiver = new DovecotMailDeliverer(); +    this.sender = new AwsMailDeliverer(aws);    }  } diff --git a/services/docker/mail-server/aws-sendmail/deno.json b/services/docker/mail-server/aws-sendmail/deno.json index 81318fa..67dd7d1 100644 --- a/services/docker/mail-server/aws-sendmail/deno.json +++ b/services/docker/mail-server/aws-sendmail/deno.json @@ -8,6 +8,7 @@      "@aws-sdk/client-sesv2": "npm:@aws-sdk/client-sesv2@^3.782.0",      "@oak/oak": "jsr:@oak/oak@^17.1.4",      "@std/cli": "jsr:@std/cli@^1.0.17", +    "@std/encoding": "jsr:@std/encoding@^1.0.10",      "@std/path": "jsr:@std/path@^1.0.9",      "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 2567551..2731eb5 100644 --- a/services/docker/mail-server/aws-sendmail/deno.lock +++ b/services/docker/mail-server/aws-sendmail/deno.lock @@ -1419,6 +1419,7 @@      "dependencies": [        "jsr:@oak/oak@^17.1.4",        "jsr:@std/cli@^1.0.17", +      "jsr:@std/encoding@^1.0.10",        "jsr:@std/path@^1.0.9",        "npm:@aws-sdk/client-s3@^3.797.0",        "npm:@aws-sdk/client-sesv2@^3.782.0", diff --git a/services/docker/mail-server/aws-sendmail/logger.ts b/services/docker/mail-server/aws-sendmail/logger.ts index 12dbc80..8cea3b0 100644 --- a/services/docker/mail-server/aws-sendmail/logger.ts +++ b/services/docker/mail-server/aws-sendmail/logger.ts @@ -13,7 +13,10 @@ function generateTimeStringForFileName(  }  export class Logger { -  constructor(public readonly path: string) { +  constructor(public readonly path: string) {} + +  warn(message: string) { +    console.log(message);    }    generateLogFilePath( @@ -53,3 +56,16 @@ export class Logger {      process.stderr.pipeTo(stderrFile.writable);    }  } + +let _logger: Logger | null = null; + +export function getLogger(): Logger { +  if (_logger == null) { +    throw new Error("No logger is set now."); +  } +  return _logger; +} + +export function setLogger(logger: Logger | null) { +  _logger = logger; +} diff --git a/services/docker/mail-server/aws-sendmail/mail.ts b/services/docker/mail-server/aws-sendmail/mail.ts new file mode 100644 index 0000000..e673593 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/mail.ts @@ -0,0 +1,42 @@ +import { encodeBase64 } from "@std/encoding/base64"; +import { getLogger } from "./logger.ts"; + +export class Mail { +  message_id: string | null = null; +  aws_message_id: string | null = null; + +  constructor(public raw: string) {} + +  encodeUtf8(): Uint8Array { +    const utf8Encoder = new TextEncoder(); +    // TODO: A problem here is if mail is VERY long, this will block for a long time. +    // Maybe some task queue can be used. +    return utf8Encoder.encode(this.raw); +  } + +  getRawBase64(): string { +    return encodeBase64(this.raw); +  } + +  appendHeaders( +    rawMail: string, +    headers: [key: string, value: string][], +  ): string { +    const separatorMatch = rawMail.match(/(\r\n|\n)(\r\n|\n)/); +    if (separatorMatch == null) { +      throw new Error( +        "No header/body separator (2 successive EOLs) found. Cannot append headers.", +      ); +    } + +    if (separatorMatch[1] !== separatorMatch[2]) { +      getLogger().warn("Different EOLs (\\r\\n and \\n) found in mail!"); +    } + +    const headerStr = headers.map(([k, v]) => `${k}: ${v}${separatorMatch[1]}`) +      .join(""); +    const endOfHeadersIndex = separatorMatch.index! + separatorMatch[1].length; +    return rawMail.slice(0, endOfHeadersIndex) + headerStr + +      rawMail.slice(endOfHeadersIndex); +  } +} diff --git a/services/docker/mail-server/aws-sendmail/main.ts b/services/docker/mail-server/aws-sendmail/main.ts index 3a73a6f..98e364e 100644 --- a/services/docker/mail-server/aws-sendmail/main.ts +++ b/services/docker/mail-server/aws-sendmail/main.ts @@ -1,5 +1,6 @@ +import { AwsContext } from "./aws.ts";  import { MailTrafficDeliverer } from "./delivers/traffic.ts"; -import { Logger } from "./logger.ts"; +import { Logger, setLogger } from "./logger.ts";  class BugError extends Error {  } @@ -7,35 +8,12 @@ class BugError extends Error {  function warn(message: string) {  } -class MailProcessor { -  appendHeaders( -    rawMail: string, -    headers: [key: string, value: string][], -  ): string { -    const separatorMatch = rawMail.match(/(\r\n|\n)(\r\n|\n)/); -    if (separatorMatch == null) { -      throw new Error( -        "No header/body separator (2 successive EOLs) found. Cannot append headers.", -      ); -    } - -    if (separatorMatch[1] !== separatorMatch[2]) { -      warn("Different EOLs (\\r\\n and \\n) found in mail!"); -    } - -    const headerStr = headers.map(([k, v]) => `${k}: ${v}${separatorMatch[1]}`) -      .join(""); -    const endOfHeadersIndex = separatorMatch.index! + separatorMatch[1].length; -    return rawMail.slice(0, endOfHeadersIndex) + headerStr + -      rawMail.slice(endOfHeadersIndex); -  } -} -  class App { -  readonly logger = new Logger("log"); -  readonly mailTrafficDeliverer = new MailTrafficDeliverer(this.logger); +  readonly aws = new AwsContext(); +  readonly mailTrafficDeliverer = new MailTrafficDeliverer(this.aws);    constructor() { +    setLogger(new Logger("log"));    }  } | 
