225 lines · 9.2 KB
import { env } from "cloudflare:workers";
import { publicUrl } from "./events/emit";
import type { QueueMessage } from "./events/emit";
import { CATEGORY_INFO } from "~/lib/notification-categories";
import type { NotificationCategory } from "~/lib/notification-categories";
import { captureDevMail, devMailMode } from "./notify/dev-mail";

export type EmailQueueMessage = Extract<QueueMessage, { kind: "email.send" }>;
export type EmailContent = { subject: string; text: string; html: string; headers?: Record<string, string> };
type Action = { label: string; url: string };
/** Links appended to every notification email; `unsubscribeUrl` also drives the List-Unsubscribe headers. */
export type EmailFooter = { reason: string; manageUrl: string; unsubscribeUrl: string | null };
export type DigestEntry = { title: string; url: string };

const HTML_ESCAPES: Record<string, string> = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
const MAX_DETAIL_LENGTH = 500;

function escapeHtml(value: string): string {
  return value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char] ?? char);
}

function forgeName(): string {
  return env.FORGE_NAME || "gitfor";
}

function clip(value: string): string {
  return value.length > MAX_DETAIL_LENGTH ? `${value.slice(0, MAX_DETAIL_LENGTH)}…` : value;
}

function footerHtml(footer: EmailFooter | null): string {
  if (!footer) return "";
  const unsubscribe = footer.unsubscribeUrl
    ? ` · <a href="${escapeHtml(footer.unsubscribeUrl)}" style="color:#a1a1aa">Unsubscribe</a>`
    : "";
  return `<hr style="border:0;border-top:1px solid #27272a;margin:24px 0 12px">
<p style="margin:0;color:#71717a;font-size:12px">${escapeHtml(footer.reason)}<br>
<a href="${escapeHtml(footer.manageUrl)}" style="color:#a1a1aa">Manage notification settings</a>${unsubscribe}</p>`;
}

type RenderInput = {