diff options
12 files changed, 188 insertions, 86 deletions
diff --git a/services/docker/mail-server/aws-sendmail/aws/base.ts b/services/docker/mail-server/aws-sendmail/aws/base.ts deleted file mode 100644 index 1e23009..0000000 --- a/services/docker/mail-server/aws-sendmail/aws/base.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getEnvRequired } from "../base.ts"; - -export class AwsContext { - readonly region = "ap-southeast-1"; - - accessKeyId = getEnvRequired("AWS_USER", "aws access key id"); - secretAccessKey = getEnvRequired("AWS_PASSWORD", "aws secret access key"); - - getCredentials() { - const { accessKeyId, secretAccessKey } = this; - return Promise.resolve({ accessKeyId, secretAccessKey }); - } - - readonly credentials = this.getCredentials.bind(this); -} diff --git a/services/docker/mail-server/aws-sendmail/aws/context.ts b/services/docker/mail-server/aws-sendmail/aws/context.ts new file mode 100644 index 0000000..65c8371 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/aws/context.ts @@ -0,0 +1,41 @@ +import { + CopyObjectCommand, + DeleteObjectCommand, + S3Client, +} from "@aws-sdk/client-s3"; + +import { getConfig } from "../config.ts"; + +export class AwsContext { + readonly region = "ap-southeast-1"; + + accessKeyId = getConfig().getValue("awsAccessKeyId"); + secretAccessKey = getConfig().getValue("awsSecretAccessKey"); + + getCredentials() { + const { accessKeyId, secretAccessKey } = this; + return Promise.resolve({ accessKeyId, secretAccessKey }); + } + + readonly credentials = this.getCredentials.bind(this); +} + +export async function s3MoveObject( + client: S3Client, + bucket: string, + path: string, + newPath: string, +): Promise<void> { + const copyCommand = new CopyObjectCommand({ + Bucket: bucket, + Key: newPath, + CopySource: path, + }); + await client.send(copyCommand); + + const deleteCommand = new DeleteObjectCommand({ + Bucket: bucket, + Key: path, + }); + await client.send(deleteCommand); +} diff --git a/services/docker/mail-server/aws-sendmail/aws/deliver.ts b/services/docker/mail-server/aws-sendmail/aws/deliver.ts index da100f5..5982d56 100644 --- a/services/docker/mail-server/aws-sendmail/aws/deliver.ts +++ b/services/docker/mail-server/aws-sendmail/aws/deliver.ts @@ -1,6 +1,6 @@ import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2"; -import { AwsContext } from "./base.ts"; +import { AwsContext } from "./context.ts"; import { Mail, MailDeliverer } from "../mail.ts"; export class AwsMailDeliverer extends MailDeliverer { diff --git a/services/docker/mail-server/aws-sendmail/aws/retriver.ts b/services/docker/mail-server/aws-sendmail/aws/retriver.ts index cdfe6f1..1544c04 100644 --- a/services/docker/mail-server/aws-sendmail/aws/retriver.ts +++ b/services/docker/mail-server/aws-sendmail/aws/retriver.ts @@ -6,23 +6,23 @@ import { S3Client, } from "@aws-sdk/client-s3"; -import { generateTimeStringForFileName, getEnvRequired } from "../base.ts"; +import { generateTimeStringForFileName } from "../util.ts"; import { getLogger } from "../logger.ts"; -import { AwsContext } from "./base.ts"; -import { MailDeliverer } from "../mail.ts"; +import { AwsContext, s3MoveObject } from "./context.ts"; +import { getConfig } from "../config.ts"; export class AwsMailRetriever { - mailBucket = getEnvRequired( - "AWS_MAIL_BUCKET", - "aws s3 bucket saving raw mails", - ); - liveMailPrefix = "mail/live/"; - archiveMailPrefix = "mail/archive/"; + readonly liveMailPrefix = "mail/live/"; + readonly archiveMailPrefix = "mail/archive/"; + readonly mailBucket = getConfig().getValue("awsMailBucket"); - private s3Client; - private liveMailRecyclerAborter = new AbortController(); + private readonly s3Client; + private readonly liveMailRecyclerAborter = new AbortController(); - constructor(private aws: AwsContext, private localDeliverer: MailDeliverer) { + constructor( + aws: AwsContext, + private readonly callback: (rawMail: string) => Promise<void>, + ) { const { region, credentials } = aws; this.s3Client = new S3Client({ region, credentials }); } @@ -66,9 +66,10 @@ export class AwsMailRetriever { } async deliverS3MailObject(messageId: string) { + const mailPath = `${this.liveMailPrefix}${messageId}`; const command = new GetObjectCommand({ Bucket: this.mailBucket, - Key: `${this.liveMailPrefix}${messageId}`, + Key: mailPath, }); const res = await this.s3Client.send(command); @@ -78,9 +79,10 @@ export class AwsMailRetriever { } const rawMail = await res.Body.transformToString(); - await this.localDeliverer.deliverRaw(rawMail); + await this.callback(rawMail); - const archiveCommand = new + // TODO: Continue here. + await s3MoveObject(this.s3Client, this.mailBucket, mailPath, ); } async recycleLiveMails() { diff --git a/services/docker/mail-server/aws-sendmail/base.ts b/services/docker/mail-server/aws-sendmail/base.ts deleted file mode 100644 index 08b592a..0000000 --- a/services/docker/mail-server/aws-sendmail/base.ts +++ /dev/null @@ -1,34 +0,0 @@ -export const APP_PREFIX = "crupest"; -export const APP_NAME = "mailserver"; - -export function getEnvRequired(key: string, usage: string): string { - key = `${APP_PREFIX.toUpperCase()}_${APP_NAME.toUpperCase()}_${key}`; - const value = Deno.env.get(key); - if (value == null) { - throw new Error(`Env ${key} does not exist, used for ${usage}.`); - } - return value; -} - -function getZonedDateTime(instant?: Temporal.Instant | Date) { - if (instant == null) { - instant = Temporal.Now.instant(); - } else if (instant instanceof Date) { - instant = instant.toTemporalInstant(); - } - - return instant.toZonedDateTimeISO("UTC"); -} - -export function generateTimeStringForFileName( - instant?: Temporal.Instant | Date, - dateOnly: boolean = false, -): string { - const time = getZonedDateTime(instant); - - if (dateOnly) { - return time.toPlainDate().toString(); - } else { - return time.toPlainDateTime().toString().replaceAll(/:|\./g, "-"); - } -} diff --git a/services/docker/mail-server/aws-sendmail/config.ts b/services/docker/mail-server/aws-sendmail/config.ts new file mode 100644 index 0000000..0212029 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/config.ts @@ -0,0 +1,56 @@ +import { createSingleton, transformProperties } from "./util.ts"; + +export const APP_PREFIX = "crupest"; +export const APP_NAME = "mailserver"; + +interface ConfigItemDef { + env: string; + description: string; + default?: string; +} + +export const CONFIG_DEFS = { + awsAccessKeyId: { env: "AWS_USER", description: "aws access key id" }, + awsSecretAccessKey: { + env: "AWS_PASSWORD", + description: "aws secret access key", + }, + awsMailBucket: { + env: "AWS_MAIL_BUCKET", + description: "aws s3 bucket saving raw mails", + }, +} as const satisfies Record<string, ConfigItemDef>; + +type ConfigDefs = typeof CONFIG_DEFS; +type ConfigKey = keyof ConfigDefs; +type ConfigMap = { + [K in ConfigKey]: ConfigDefs[K] & { value: string }; +}; + +function resolveAppConfigItem(def: ConfigItemDef): string { + const envKey = + `${APP_PREFIX.toUpperCase()}_${APP_NAME.toUpperCase()}_${def.env}`; + const value = Deno.env.get(envKey); + if (value != null) return value; + if (def.default != null) return def.default; + throw new Error( + `Required env ${envKey} (${def.description}) is not set.`, + ); +} + +export class Config { + private config = transformProperties( + CONFIG_DEFS, + (def) => ({ ...def, value: resolveAppConfigItem(def) }), + ) as ConfigMap; + + get<K extends ConfigKey>(key: K): ConfigMap[K] { + return this.config[key]; + } + + getValue(key: ConfigKey): string { + return this.get(key).value; + } +} + +export const [getConfig, setConfig] = createSingleton<Config>("Config"); diff --git a/services/docker/mail-server/aws-sendmail/db.ts b/services/docker/mail-server/aws-sendmail/db.ts index 6a7b100..e239e72 100644 --- a/services/docker/mail-server/aws-sendmail/db.ts +++ b/services/docker/mail-server/aws-sendmail/db.ts @@ -72,7 +72,14 @@ export class DbService { private _migrator; constructor(public readonly path: string) { - this._sqlocal = new SQLocalKysely("database.sqlite3"); + this._sqlocal = new SQLocalKysely({ + databasePath: path, + onInit: (sql) => [ + // Though this can be executed only once when database is + // created, re-calls should not affect performance. + sql`PRAGMA journal_mode=WAL;`, + ], + }); const db = new Kysely<Database>({ dialect: this._sqlocal.dialect }); this._db = db; this._migrator = new Migrator({ diff --git a/services/docker/mail-server/aws-sendmail/logger.ts b/services/docker/mail-server/aws-sendmail/logger.ts index 3a0cff6..160d70a 100644 --- a/services/docker/mail-server/aws-sendmail/logger.ts +++ b/services/docker/mail-server/aws-sendmail/logger.ts @@ -1,7 +1,6 @@ import * as path from "@std/path"; -import { generateTimeStringForFileName } from "./base.ts"; - +import { createSingleton, generateTimeStringForFileName } from "./util.ts"; export class Logger { constructor(public readonly path: string) {} @@ -48,15 +47,4 @@ export class Logger { } } -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; -} +export const [getLogger, setLogger] = createSingleton<Logger>("Logger"); diff --git a/services/docker/mail-server/aws-sendmail/mail.ts b/services/docker/mail-server/aws-sendmail/mail.ts index 82902d2..d4bcf75 100644 --- a/services/docker/mail-server/aws-sendmail/mail.ts +++ b/services/docker/mail-server/aws-sendmail/mail.ts @@ -113,11 +113,17 @@ export class Mail { simpleParseHeaders(): [key: string, value: string][] { const { headerStr } = this.simpleParse(); - const rawLines = headerStr.split(/\r\n|\n|\r/); - const headerLines = []; - for (const l of rawLines) { - + const lines: string[] = []; + for (const line of headerStr.split(/\r?\n|\r/)) { + if (line.match(/^\s/)) { + if (lines.length === 0) { + throw new MailParseError("Header part starts with a space.", this); + } + lines[lines.length - 1] += line; + } } + + // TODO: Continue here. } appendHeaders(headers: [key: string, value: string][]) { diff --git a/services/docker/mail-server/aws-sendmail/main.ts b/services/docker/mail-server/aws-sendmail/main.ts index 7207188..dcc2af1 100644 --- a/services/docker/mail-server/aws-sendmail/main.ts +++ b/services/docker/mail-server/aws-sendmail/main.ts @@ -1,6 +1,7 @@ -import { AwsContext } from "./aws/base.ts"; +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(); @@ -8,6 +9,7 @@ class App { constructor() { setLogger(new Logger("log")); + setConfig(new Config()); } } diff --git a/services/docker/mail-server/aws-sendmail/traffic.ts b/services/docker/mail-server/aws-sendmail/traffic.ts index 4e92557..7ecc405 100644 --- a/services/docker/mail-server/aws-sendmail/traffic.ts +++ b/services/docker/mail-server/aws-sendmail/traffic.ts @@ -1,7 +1,7 @@ import { MailDeliverer } from "./mail.ts"; import { DbService } from "./db.ts"; import { DovecotMailDeliverer } from "./dovecot.ts"; -import { AwsContext } from "./aws/base.ts"; +import { AwsContext } from "./aws/context.ts"; import { AwsMailDeliverer } from "./aws/deliver.ts"; export abstract class MailTrafficHandler { diff --git a/services/docker/mail-server/aws-sendmail/util.ts b/services/docker/mail-server/aws-sendmail/util.ts new file mode 100644 index 0000000..1411840 --- /dev/null +++ b/services/docker/mail-server/aws-sendmail/util.ts @@ -0,0 +1,49 @@ +function getZonedDateTime(instant?: Temporal.Instant | Date) { + if (instant == null) { + instant = Temporal.Now.instant(); + } else if (instant instanceof Date) { + instant = instant.toTemporalInstant(); + } + + return instant.toZonedDateTimeISO("UTC"); +} + +export function generateTimeStringForFileName( + instant?: Temporal.Instant | Date, + dateOnly: boolean = false, +): string { + const time = getZonedDateTime(instant); + + if (dateOnly) { + return time.toPlainDate().toString(); + } else { + return time.toPlainDateTime().toString().replaceAll(/:|\./g, "-"); + } +} + +export function transformProperties<T extends object, N>( + object: T, + transformer: (value: T[keyof T], key: keyof T) => N, +): { [k in keyof T]: N } { + return Object.fromEntries( + Object.entries(object).map(( + [k, v], + ) => [k, transformer(v, k as keyof T)]), + ) as { [k in keyof T]: N }; +} + +export function createSingleton<T>( + name: string, +): [() => T, (v: T | null) => void] { + let singleton: T | null = null; + + return [ + () => { + if (singleton == null) { + throw new Error(`Singleton ${name} is not set now.`); + } + return singleton; + }, + (newValue: T | null) => singleton = newValue, + ]; +} |