import { env } from "cloudflare:workers";
import { randomId } from "../crypto";
import type { EmailQueueMessage } from "../email";
const KEY_PREFIX = "devmail:";
const TTL_SECONDS = 24 * 60 * 60;
const MAX_LISTED = 100;
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
const HTML_ESCAPES: Record<string, string> = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
type DevMailMode = "log" | "kv";
type CapturedMail = EmailQueueMessage & { id: string; capturedAt: number };
/** The forge counts as a dev instance only when its public URL is a loopback host; production never is. */
export function isDevForge(): boolean {
try {
const host = new URL(env.PUBLIC_URL).hostname.toLowerCase();
return LOCAL_HOSTS.has(host) || host.endsWith(".localhost");
} catch {
return false;
}
}
/** EMAIL_DEV_SINK=log|kv picks the sink; a dev forge without a deliverable sender defaults to kv. */
export function devMailMode(deliverable: boolean): DevMailMode | null {
if (!isDevForge()) return null;
const configured = env.EMAIL_DEV_SINK;
if (configured === "log" || configured === "kv") return configured;
return deliverable ? null : "kv";
}
export async function captureDevMail(mode: DevMailMode, message: EmailQueueMessage): Promise<void> {
if (mode === "log") {
console.warn("[dev mail]", JSON.stringify({ to: message.to, subject: message.subject, headers: message.headers }));
console.warn(message.text);
return;
}
const capturedAt = Date.now();
const id = `${String(capturedAt).padStart(15, "0")}-${randomId()}`;
const record: CapturedMail = { ...message, id, capturedAt };