1import type { Database } from "bun:sqlite";2import { createTransport } from "nodemailer";3import type { Config, SmtpSettings } from "./config.ts";45const LOOP_MS = 60 * 1000;67const RETRY_MINUTES = [1, 5, 30, 120, 720];8const TIMEOUT_MS = 15 * 1000;91011export async function sendMail(smtp: SmtpSettings, subject: string, text: string): Promise<void> {12 const transport = createTransport({13 host: smtp.host,14 port: smtp.port,15 secure: smtp.security === "tls",16 requireTLS: smtp.security === "starttls",17 ignoreTLS: smtp.security === "none",18 ...(smtp.username ? { auth: { user: smtp.username, pass: smtp.password } } : {}),19 connectionTimeout: TIMEOUT_MS,20 greetingTimeout: TIMEOUT_MS,21 socketTimeout: TIMEOUT_MS,22 });23 try {24 await transport.sendMail({25 from: smtp.from,26 to: smtp.to,27 28 subject: `kepr: ${subject}`.replace(/[\r\n]+/g, " ").slice(0, 200),29 text,30 });31 } finally {32 transport.close();33 }34}3536type Queued = { id: number; subject: string; href: string; attempts: number };3738let running: Promise<void> | null = null;394041export function deliverDue(kepr: Database, config: Config, now = Date.now()): Promise<void> {42 running ??= deliver(kepr, config, Math.floor(now / 1000)).finally(() => {43 running = null;44 });45 return running;46}4748async function deliver(kepr: Database, config: Config, now: number): Promise<void> {49 const due = kepr50 .query<Queued, [number]>(51 `SELECT id, subject, href, attempts FROM email_queue52 WHERE status = 'waiting' AND next_at <= ?53 ORDER BY id`,54 )55 .all(now);56 for (const email of due) {57 try {58 if (!config.smtp) throw new Error("no [smtp] settings in kepr.toml");59 const link = `https://${config.domain}${email.href}`;60 await sendMail(config.smtp, email.subject, `${email.subject}\n\n${link}\n`);61 kepr62 .query("UPDATE email_queue SET status = 'sent', attempts = ?, sent_at = ? WHERE id = ?")63 .run(email.attempts + 1, now, email.id);64 } catch (error) {65 const attempts = email.attempts + 1;66 const wait = RETRY_MINUTES[attempts - 1];67 kepr68 .query(69 `UPDATE email_queue SET status = ?, attempts = ?, next_at = ?, last_error = ?70 WHERE id = ?`,71 )72 .run(73 wait === undefined ? "failed" : "waiting",74 attempts,75 now + (wait ?? 0) * 60,76 error instanceof Error ? error.message : String(error),77 email.id,78 );79 }80 }81}8283export function startMailLoop(kepr: Database, config: Config): void {84 setInterval(() => {85 deliverDue(kepr, config).catch((error) => console.error(`kepr: email failed: ${error}`));86 }, LOOP_MS);87}