296 lines · 12.5 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";
import { captureServerEvent } from "./analytics";

export type EmailQueueMessage = Extract<QueueMessage, { kind: "email.send" }>;
export type EmailContent = {
  subject: string;
  text: string;
  html: string;
  headers?: Record<string, string>;
  /** Address replies go to; set on thread notifications so a reply becomes a comment. */
  replyTo?: 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";
}

const MAX_EXCERPT_LENGTH = 2000;

function clipExcerpt(value: string): string {
  const trimmed = value.trim();
  return trimmed.length > MAX_EXCERPT_LENGTH ? `${trimmed.slice(0, MAX_EXCERPT_LENGTH)}…` : trimmed;
}