146 lines · 6.1 KB
import { env } from "cloudflare:workers";
import { randomId } from "../crypto";
import type { EmailQueueMessage } from "../email";

const KEY_PREFIX = "devmail:";
export const DEV_MAIL_MESSAGES_PATH = "/__dev/mail/messages";
const TTL_SECONDS = 24 * 60 * 60;
const MAX_LISTED = 100;
const MAX_CLEAR_PAGES = 20;
/** Keys sort newest first (KV lists ascending), so the first page always holds the latest mail. */
const MAX_TIMESTAMP = 9_999_999_999_999;
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
const HTML_ESCAPES: Record<string, string> = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };

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;
  }
}

/** Opt-in only: EMAIL_DEV_SINK=log|kv on a forge whose PUBLIC_URL is a loopback host. */
export function devMailMode(): DevMailMode | null {
  if (!isDevForge()) return null;
  const configured = env.EMAIL_DEV_SINK;
  return configured === "log" || configured === "kv" ? configured : null;
}

/** Viewer link for mail sent to `to`, only when the KV sink is capturing it instead of delivering. */
export function devMailPreviewUrl(to: string): string | null {
  if (devMailMode() !== "kv") return null;
  return `/__dev/mail?to=${encodeURIComponent(to.toLowerCase())}`;
}