diff options
Diffstat (limited to 'deno/mail-relay')
-rw-r--r-- | deno/mail-relay/app.ts | 11 | ||||
-rw-r--r-- | deno/mail-relay/aws/deliver.ts | 6 | ||||
-rw-r--r-- | deno/mail-relay/db.ts | 45 | ||||
-rw-r--r-- | deno/mail-relay/dovecot.ts | 15 | ||||
-rw-r--r-- | deno/mail-relay/dumb-smtp-server.ts | 45 | ||||
-rw-r--r-- | deno/mail-relay/mail.test.ts | 26 | ||||
-rw-r--r-- | deno/mail-relay/mail.ts | 70 |
7 files changed, 118 insertions, 100 deletions
diff --git a/deno/mail-relay/app.ts b/deno/mail-relay/app.ts index 3cac44b..d96fa1d 100644 --- a/deno/mail-relay/app.ts +++ b/deno/mail-relay/app.ts @@ -43,7 +43,7 @@ export function createHono( const hono = new Hono(); hono.onError((err, c) => { - logger.error(err); + logger.error("Hono handler throws an error.", err); return c.json({ msg: "Server error, check its log." }, 500); }); hono.use(honoLogger()); @@ -81,10 +81,9 @@ export async function sendMail(logger: Logger, port: number) { method: "post", body: text, }); - logger.builder(res).setError(!res.ok).write(); - logger - .builder("Body\n" + (await res.text())) - .setError(!res.ok) - .write(); + logger.write(Deno.inspect(res), { level: res.ok ? "info" : "error" }); + logger.write(Deno.inspect(await res.text()), { + level: res.ok ? "info" : "error", + }); if (!res.ok) Deno.exit(-1); } diff --git a/deno/mail-relay/aws/deliver.ts b/deno/mail-relay/aws/deliver.ts index 3e1f162..9950e37 100644 --- a/deno/mail-relay/aws/deliver.ts +++ b/deno/mail-relay/aws/deliver.ts @@ -45,14 +45,12 @@ export class AwsMailDeliverer extends SyncMailDeliverer { if (res.MessageId == null) { this.#logger.warn("Aws send-email returns no message id."); } else { - context.result.awsMessageId = - `${res.MessageId}@${this.#aws.region}.amazonses.com`; + context.result.awsMessageId = `${res.MessageId}@${this.#aws.region}.amazonses.com`; } context.result.recipients.set("*", { kind: "done", - message: - `Successfully called aws send-email, message id ${context.result.awsMessageId}.`, + message: `Successfully called aws send-email, message id ${context.result.awsMessageId}.`, }); } catch (cause) { context.result.recipients.set("*", { diff --git a/deno/mail-relay/db.ts b/deno/mail-relay/db.ts index 9b05e32..807ecf6 100644 --- a/deno/mail-relay/db.ts +++ b/deno/mail-relay/db.ts @@ -20,19 +20,20 @@ class SqliteStatementAdapter implements SqliteStatement { } all(parameters: readonly unknown[]): unknown[] { - return this.stmt.all(...parameters as sqlite.BindValue[]); + return this.stmt.all(...(parameters as sqlite.BindValue[])); } iterate(parameters: readonly unknown[]): IterableIterator<unknown> { - return this.stmt.iter(...parameters as sqlite.BindValue[]); + return this.stmt.iter(...(parameters as sqlite.BindValue[])); } - run( - parameters: readonly unknown[], - ): { changes: number | bigint; lastInsertRowid: number | bigint } { + run(parameters: readonly unknown[]): { + changes: number | bigint; + lastInsertRowid: number | bigint; + } { const { db } = this.stmt; const totalChangesBefore = db.totalChanges; - const changes = this.stmt.run(...parameters as sqlite.BindValue[]); + const changes = this.stmt.run(...(parameters as sqlite.BindValue[])); return { changes: totalChangesBefore === db.totalChanges ? 0 : changes, lastInsertRowid: db.lastInsertRowId, @@ -52,8 +53,7 @@ class SqliteDatabaseAdapter implements SqliteDatabase { } } -export class DbError extends Error { -} +export class DbError extends Error {} interface AwsMessageIdMapTable { id: Generated<number>; @@ -100,9 +100,9 @@ export class DbService { constructor(public readonly path: string) { this.#db = new sqlite.Database(path); this.#kysely = new Kysely<Database>({ - dialect: new SqliteDialect( - { database: new SqliteDatabaseAdapter(this.#db) }, - ), + dialect: new SqliteDialect({ + database: new SqliteDatabaseAdapter(this.#db), + }), }); this.#migrator = new Migrator({ db: this.#kysely, @@ -121,24 +121,27 @@ export class DbService { async addMessageIdMap( mail: Insertable<AwsMessageIdMapTable>, ): Promise<number> { - const inserted = await this.#kysely.insertInto("aws_message_id_map").values( - mail, - ).executeTakeFirstOrThrow(); + const inserted = await this.#kysely + .insertInto("aws_message_id_map") + .values(mail) + .executeTakeFirstOrThrow(); return Number(inserted.insertId!); } async messageIdToAws(messageId: string): Promise<string | null> { - const row = await this.#kysely.selectFrom("aws_message_id_map").where( - "message_id", - "=", - messageId, - ).select("aws_message_id").executeTakeFirst(); + const row = await this.#kysely + .selectFrom("aws_message_id_map") + .where("message_id", "=", messageId) + .select("aws_message_id") + .executeTakeFirst(); return row?.aws_message_id ?? null; } async messageIdFromAws(awsMessageId: string): Promise<string | null> { - const row = await this.#kysely.selectFrom("aws_message_id_map") - .where("aws_message_id", "=", awsMessageId).select("message_id") + const row = await this.#kysely + .selectFrom("aws_message_id_map") + .where("aws_message_id", "=", awsMessageId) + .select("message_id") .executeTakeFirst(); return row?.message_id ?? null; } diff --git a/deno/mail-relay/dovecot.ts b/deno/mail-relay/dovecot.ts index cb63766..124a82b 100644 --- a/deno/mail-relay/dovecot.ts +++ b/deno/mail-relay/dovecot.ts @@ -2,11 +2,7 @@ import { basename } from "@std/path"; import { Logger } from "@crupest/base/log"; -import { - Mail, - MailDeliverContext, - MailDeliverer, -} from "./mail.ts"; +import { Mail, MailDeliverContext, MailDeliverer } from "./mail.ts"; export class DovecotMailDeliverer extends MailDeliverer { readonly name = "dovecot"; @@ -38,9 +34,7 @@ export class DovecotMailDeliverer extends MailDeliverer { for (const recipient of recipients) { try { const commandArgs = ["-d", recipient]; - this.logger.info( - `Run ${ldaBinName} ${commandArgs.join(" ")}...`, - ); + this.logger.info(`Run ${ldaBinName} ${commandArgs.join(" ")}...`); const ldaCommand = new Deno.Command(ldaPath, { args: commandArgs, @@ -50,9 +44,8 @@ export class DovecotMailDeliverer extends MailDeliverer { }); const ldaProcess = ldaCommand.spawn(); - using logFiles = await this.logger.createExternalLogStreamsForProgram( - ldaBinName, - ); + using logFiles = + await this.logger.createExternalLogStreamsForProgram(ldaBinName); ldaProcess.stdout.pipeTo(logFiles.stdout); ldaProcess.stderr.pipeTo(logFiles.stderr); diff --git a/deno/mail-relay/dumb-smtp-server.ts b/deno/mail-relay/dumb-smtp-server.ts index 66c2f7c..1a1090a 100644 --- a/deno/mail-relay/dumb-smtp-server.ts +++ b/deno/mail-relay/dumb-smtp-server.ts @@ -17,6 +17,8 @@ function createResponses(host: string, port: number | string) { } as const; } +const LOG_TAG = "[dumb-smtp]"; + export class DumbSmtpServer { #logger; #deliverer; @@ -33,9 +35,12 @@ export class DumbSmtpServer { async #handleConnection(conn: Deno.Conn) { using disposeStack = new DisposableStack(); disposeStack.defer(() => { - this.#logger.info("Close smtp session tcp connection."); + this.#logger.tagInfo(LOG_TAG, "Close session's tcp connection."); conn.close(); }); + + this.#logger.tagInfo(LOG_TAG, "New session's tcp connection established."); + const writer = conn.writable.getWriter(); disposeStack.defer(() => writer.releaseLock()); const reader = conn.readable.getReader(); @@ -43,8 +48,10 @@ export class DumbSmtpServer { const [decoder, encoder] = [new TextDecoder(), new TextEncoder()]; const decode = (data: Uint8Array) => decoder.decode(data); - const send = async (s: string) => + const send = async (s: string) => { + this.#logger.tagInfo(LOG_TAG, "Send line: " + s); await writer.write(encoder.encode(s + CRLF)); + }; let buffer: string = ""; let rawMail: string | null = null; @@ -65,7 +72,7 @@ export class DumbSmtpServer { buffer = buffer.slice(eolPos + CRLF.length); if (rawMail == null) { - this.#logger.info("Smtp server received line:", line); + this.#logger.tagInfo(LOG_TAG, "Received line: " + line); const upperLine = line.toUpperCase(); if (upperLine.startsWith("EHLO") || upperLine.startsWith("HELO")) { await send(this.#responses["EHLO"]); @@ -75,26 +82,32 @@ export class DumbSmtpServer { await send(this.#responses["RCPT"]); } else if (upperLine === "DATA") { await send(this.#responses["DATA"]); - this.#logger.info("Begin to receive mail data..."); + this.#logger.tagInfo(LOG_TAG, "Begin to receive mail data..."); rawMail = ""; } else if (upperLine === "QUIT") { await send(this.#responses["QUIT"]); return; } else { - this.#logger.warn("Smtp server command unrecognized:", line); + this.#logger.tagWarn( + LOG_TAG, + "Unrecognized command from client: " + line, + ); await send(this.#responses["INVALID"]); return; } } else { if (line === ".") { try { - this.#logger.info("Done receiving mail data, begin to relay..."); + this.#logger.tagInfo( + LOG_TAG, + "Mail data Received, begin to relay...", + ); const { message } = await this.#deliverer.deliverRaw(rawMail); await send(`250 2.6.0 ${message}`); rawMail = null; - this.#logger.info("Done SMTP mail session."); + this.#logger.tagInfo(LOG_TAG, "Relay succeeded."); } catch (err) { - this.#logger.info(err); + this.#logger.tagError(LOG_TAG, "Relay failed.", err); await send("554 5.3.0 Error: check server log"); return; } @@ -107,21 +120,23 @@ export class DumbSmtpServer { } } - async serve(options: { - hostname: string, - port: number - }) { + async serve(options: { hostname: string; port: number }) { const listener = Deno.listen(options); this.#responses = createResponses(options.hostname, options.port); - this.#logger.info( - `Dumb SMTP server starts running on ${this.#responses.serverName}.`, + this.#logger.tagInfo( + LOG_TAG, + `Dumb SMTP server starts to listen on ${this.#responses.serverName}.`, ); for await (const conn of listener) { try { await this.#handleConnection(conn); } catch (cause) { - this.#logger.error("One smtp connection session throws an error " + cause); + this.#logger.tagError( + LOG_TAG, + "Tcp connection throws an error.", + cause, + ); } } } diff --git a/deno/mail-relay/mail.test.ts b/deno/mail-relay/mail.test.ts index 6f3cd13..09cf8eb 100644 --- a/deno/mail-relay/mail.test.ts +++ b/deno/mail-relay/mail.test.ts @@ -67,33 +67,31 @@ describe("Mail", () => { }); it("simple parse date", () => { - expect(new Mail(mockMailStr).startSimpleParse().sections().headers().date()) - .toEqual(new Date(mockDate)); + expect( + new Mail(mockMailStr).startSimpleParse().sections().headers().date(), + ).toEqual(new Date(mockDate)); }); it("simple parse headers", () => { expect( new Mail(mockMailStr).startSimpleParse().sections().headers().fields, - ).toEqual(mockHeaders.map( - (h) => [h[0], " " + h[1].replaceAll("\n", "")], - )); + ).toEqual(mockHeaders.map((h) => [h[0], " " + h[1].replaceAll("\n", "")])); }); it("parse recipients", () => { const mail = new Mail(mockMailStr); - expect([...mail.startSimpleParse().sections().headers().recipients()]) - .toEqual([ - ...mockToAddresses, - mockCcAddress, - ]); + expect([ + ...mail.startSimpleParse().sections().headers().recipients(), + ]).toEqual([...mockToAddresses, mockCcAddress]); expect([ ...mail.startSimpleParse().sections().headers().recipients({ domain: "example.com", }), - ]).toEqual([ - ...mockToAddresses, - mockCcAddress, - ].filter((a) => a.endsWith("example.com"))); + ]).toEqual( + [...mockToAddresses, mockCcAddress].filter((a) => + a.endsWith("example.com"), + ), + ); }); it("find all addresses", () => { diff --git a/deno/mail-relay/mail.ts b/deno/mail-relay/mail.ts index 8c2e067..12d5972 100644 --- a/deno/mail-relay/mail.ts +++ b/deno/mail-relay/mail.ts @@ -4,12 +4,15 @@ import emailAddresses from "email-addresses"; import { Logger } from "@crupest/base/log"; -class MailSimpleParseError extends Error { } +class MailSimpleParseError extends Error {} class MailSimpleParsedHeaders { - #logger + #logger; - constructor(logger: Logger | undefined, public fields: [key:string, value: string][]) { + constructor( + logger: Logger | undefined, + public fields: [key: string, value: string][], + ) { this.#logger = logger; } @@ -28,7 +31,9 @@ class MailSimpleParsedHeaders { if (match != null) { return match[1]; } else { - this.#logger?.warn("Invalid message-id header of mail: ", messageIdField); + this.#logger?.warn( + "Invalid message-id header of mail: " + messageIdField, + ); return undefined; } } @@ -71,10 +76,10 @@ class MailSimpleParsedSections { eol: string; sep: string; - #logger + #logger; constructor(logger: Logger | undefined, raw: string) { - this.#logger = logger + this.#logger = logger; const twoEolMatch = raw.match(/(\r?\n)(\r?\n)/); if (twoEolMatch == null) { @@ -96,7 +101,7 @@ class MailSimpleParsedSections { } headers(): MailSimpleParsedHeaders { - const headers = [] as [key:string, value: string][]; + const headers = [] as [key: string, value: string][]; let field: string | null = null; let lineNumber = 1; @@ -105,9 +110,7 @@ class MailSimpleParsedSections { if (field == null) return; const sepPos = field.indexOf(":"); if (sepPos === -1) { - throw new MailSimpleParseError( - `No ':' in the header line: ${field}`, - ); + throw new MailSimpleParseError(`No ':' in the header line: ${field}`); } headers.push([field.slice(0, sepPos).trim(), field.slice(sepPos + 1)]); field = null; @@ -149,8 +152,8 @@ export class Mail { } simpleFindAllAddresses(): string[] { - const re = /,?\<?([a-z0-9_'+\-\.]+\@[a-z0-9_'+\-\.]+)\>?,?/ig - return [...this.raw.matchAll(re)].map(m => m[1]) + const re = /,?\<?([a-z0-9_'+\-\.]+\@[a-z0-9_'+\-\.]+)\>?,?/gi; + return [...this.raw.matchAll(re)].map((m) => m[1]); } } @@ -169,16 +172,21 @@ export class MailDeliverResult { constructor(public mail: Mail) {} hasError(): boolean { - return this.recipients.size === 0 || - this.recipients.values().some((r) => r.kind !== "done"); + return ( + this.recipients.size === 0 || + this.recipients.values().some((r) => r.kind !== "done") + ); } [Symbol.for("Deno.customInspect")]() { return [ `message: ${this.message}`, - ...this.recipients.entries().map(([recipient, result]) => - `${recipient} [${result.kind}]: ${result.message}` - ), + ...this.recipients + .entries() + .map( + ([recipient, result]) => + `${recipient} [${result.kind}]: ${result.message}`, + ), ].join("\n"); } } @@ -204,7 +212,7 @@ export abstract class MailDeliverer { preHooks: MailDeliverHook[] = []; postHooks: MailDeliverHook[] = []; - constructor(protected readonly logger: Logger) { } + constructor(protected readonly logger: Logger) {} protected abstract doDeliver( mail: Mail, @@ -234,8 +242,7 @@ export abstract class MailDeliverer { await hook.callback(context); } - context.logger.info("Deliver result:"); - context.logger.info(context.result); + context.logger.info("Deliver result:\n" + Deno.inspect(context.result)); if (context.result.hasError()) { throw new Error("Mail failed to deliver."); @@ -248,13 +255,19 @@ export abstract class MailDeliverer { export abstract class SyncMailDeliverer extends MailDeliverer { #last: Promise<void> = Promise.resolve(); - override async deliver( - options: { mail: Mail; recipients?: string[] }, - ): Promise<MailDeliverResult> { - this.logger.info("The mail deliverer is sync. Wait for last delivering done..."); + override async deliver(options: { + mail: Mail; + recipients?: string[]; + }): Promise<MailDeliverResult> { + this.logger.info( + "The mail deliverer is sync. Wait for last delivering done...", + ); await this.#last; const result = super.deliver(options); - this.#last = result.then(() => {}, () => {}); + this.#last = result.then( + () => {}, + () => {}, + ); return result; } } @@ -278,8 +291,8 @@ export class RecipientFromHeadersHook implements MailDeliverHook { .forEach((r) => context.recipients.add(r)); context.logger.info( - "Recipients found from mail headers: ", - [...context.recipients].join(" "), + "Recipients found from mail headers: " + + [...context.recipients].join(", "), ); } return Promise.resolve(); @@ -292,8 +305,7 @@ export class FallbackRecipientHook implements MailDeliverHook { callback(context: MailDeliverContext) { if (context.recipients.size === 0) { context.logger.info( - "No recipients, fill with fallback: ", - [...this.fallback].join(" "), + "No recipients, fill with fallback: " + [...this.fallback].join(", "), ); this.fallback.forEach((a) => context.recipients.add(a)); } |