157 lines · 7.5 KB
import PostalMime from "postal-mime";
import { env } from "cloudflare:workers";
import { loadActiveUserById, toViewer } from "./auth";
import { hmacSha256Hex, timingSafeEqualStrings } from "./crypto";
import { repoDb } from "./scoped-db";
import { issueRepoId } from "./issue-scope";
import { HttpError } from "./errors";
import { createIssueComment } from "./issue-comments";
import type { IssueRow } from "./issues";
import { limitWrite } from "./limits";
import { loadRepoById, requireRepoMutation } from "./repos";
import { listAccountEmails } from "./user-emails";
import { extractReplyText } from "~/lib/email-reply-text";

export const REPLY_PREFIX = "reply+";
const MAC_LENGTH = 12;
const EXPIRY_LENGTH = 3;
const DAY_MS = 86_400_000;
const REPLY_TTL_DAYS = 90;
/** Cloudflare Email Routing's authserv-id; an Authentication-Results header from anyone else is ignored. */
const AUTHSERV_ID = "mx.cloudflare.net";
const MAX_LOCAL_PART = 64;
const MAX_RAW_BYTES = 1024 * 1024;
const MAX_BODY_LENGTH = 65_536;
const ID = /^[a-z0-9]+$/;
const AUTOMATED_PRECEDENCE = /^(bulk|junk|list|auto_reply)$/i;

/** EMAIL_REPLY_DOMAIN, else the sending domain of EMAIL_FROM; empty (replies off) for localhost senders. */
export function replyDomain(): string {
  const configured = (env.EMAIL_REPLY_DOMAIN ?? "").trim().toLowerCase();
  if (configured) return configured;
  const sender = /@([^>\s]+)/.exec(env.EMAIL_FROM ?? "")?.[1]?.toLowerCase() ?? "";
  return sender.includes(".") && sender !== "localhost" ? sender : "";
}

async function replyMac(userId: string, issueId: string, expiry: string): Promise<string> {
  return (await hmacSha256Hex(env.SECRETS_KEY, `email-reply:v2:${issueId}:${userId}:${expiry}`)).slice(0, MAC_LENGTH);
}

function expiryToken(now: number): string {