export const INBOX_DOMAINS = new Set([ 'inbox.kody.codes', 'inbox.heykody.app', 'inbox.heykody.dev', ]) export const INBOX_LOCAL = 'kentcdodds' export const PRIMARY_INBOX_DOMAIN = 'inbox.kody.codes' export const KENT_FROM_ADDRESSES = new Set([ 'kentcdodds@gmail.com', 'kent.c.dodds@gmail.com', 'me@kentcdodds.com', ]) export type PlusAliasHit = { address: string tag: string domain: string } export function extractEmail(raw: unknown): string { if (typeof raw !== 'string') return '' const trimmed = raw.trim() if (!trimmed) return '' const angle = trimmed.match(/<([^>]+)>/) return (angle?.[1] ?? trimmed).trim().toLowerCase() } export function isKentAddress(raw: unknown): boolean { const address = extractEmail(raw) return Boolean(address) && KENT_FROM_ADDRESSES.has(address) } export function isKentFrom(message: { from_address?: unknown envelope_from?: unknown }): boolean { return isKentAddress(message.from_address) || isKentAddress(message.envelope_from) } function listEmails(value: unknown): string[] { if (typeof value === 'string') return [extractEmail(value)].filter(Boolean) if (!Array.isArray(value)) return [] return value.map((item) => extractEmail(item)).filter(Boolean) } /** Kent is on this message as a participant, not only as the plus-alias inbox. */ export function kentOnThisMessage(message: { to_addresses?: unknown cc_addresses?: unknown bcc_addresses?: unknown reply_to_addresses?: unknown }): boolean { const addresses = [ ...listEmails(message.to_addresses), ...listEmails(message.cc_addresses), ...listEmails(message.bcc_addresses), ...listEmails(message.reply_to_addresses), ] return addresses.some((address) => KENT_FROM_ADDRESSES.has(address)) } export function plusAddressFor(tag: string): string { const normalized = tag.trim().toLowerCase() return `${INBOX_LOCAL}+${normalized}@${PRIMARY_INBOX_DOMAIN}` } export function plusAliasFromAddress(raw: unknown): PlusAliasHit | null { const email = extractEmail(raw) const at = email.lastIndexOf('@') if (at <= 0) return null const local = email.slice(0, at) const domain = email.slice(at + 1) if (!INBOX_DOMAINS.has(domain)) return null const prefix = `${INBOX_LOCAL}+` if (!local.startsWith(prefix)) return null const tag = local.slice(prefix.length).trim() if (!tag) return null return { address: email, tag, domain } } export function plusAliasesFromAddresses(addresses: unknown): PlusAliasHit[] { const list = Array.isArray(addresses) ? addresses : [] const hits: PlusAliasHit[] = [] const seen = new Set() for (const item of list) { const hit = plusAliasFromAddress(item) if (!hit) continue const key = `${hit.tag}@${hit.domain}` if (seen.has(key)) continue seen.add(key) hits.push(hit) } return hits } export function plusAliasesFromMessage(message: { to_addresses?: unknown cc_addresses?: unknown bcc_addresses?: unknown }): PlusAliasHit[] { return plusAliasesFromAddresses([ ...(Array.isArray(message.to_addresses) ? message.to_addresses : []), ...(Array.isArray(message.cc_addresses) ? message.cc_addresses : []), ...(Array.isArray(message.bcc_addresses) ? message.bcc_addresses : []), ]) } export function aliasCandidates(tag: string): string[] { const normalized = tag.trim().toLowerCase() if (!normalized) return [] const first = normalized.split('+')[0]?.trim() ?? '' const unique = [normalized] if (first && first !== normalized) unique.push(first) return unique } export function threadOpenKey(threadId: string): string { return `email-thread-open:${threadId}` } export function threadRateKey(threadId: string): string { return `email-wake-rate:${threadId}` } export const NON_KENT_WAKE_WINDOW_MS = 10 * 60 * 1000 export const NON_KENT_WAKE_MAX = 3 export const NON_KENT_WAKE_LOCKOUT_MS = 30 * 60 * 1000 export type ThreadRateState = { times?: string[] lockedUntil?: string } export type ThrottleResult = | { ok: true; state: ThreadRateState } | { ok: false; reason: 'ignored-rate-limit'; lockedUntil: string; state: ThreadRateState } function asRateState(value: unknown): ThreadRateState { let parsed: unknown = value if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed) } catch { return {} } } if (!parsed || typeof parsed !== 'object') return {} const row = parsed as ThreadRateState & { at?: unknown } const times = Array.isArray(row.times) ? row.times.filter((item): item is string => typeof item === 'string') : typeof row.at === 'string' ? [row.at] : [] return { times, lockedUntil: typeof row.lockedUntil === 'string' ? row.lockedUntil : undefined, } } /** Allow a few quick non-Kent follow-ups, then lock the thread out. */ export function consumeNonKentThreadThrottle( stored: unknown, now = Date.now(), ): ThrottleResult { const state = asRateState(stored) const lockedUntilMs = state.lockedUntil ? Date.parse(state.lockedUntil) : NaN if (Number.isFinite(lockedUntilMs) && lockedUntilMs > now) { return { ok: false, reason: 'ignored-rate-limit', lockedUntil: state.lockedUntil as string, state } } const cutoff = now - NON_KENT_WAKE_WINDOW_MS const times = (state.times || []).filter((stamp) => { const ms = Date.parse(stamp) return Number.isFinite(ms) && ms > cutoff }) if (times.length >= NON_KENT_WAKE_MAX) { const lockedUntil = new Date(now + NON_KENT_WAKE_LOCKOUT_MS).toISOString() const next = { times, lockedUntil } return { ok: false, reason: 'ignored-rate-limit', lockedUntil, state: next } } times.push(new Date(now).toISOString()) return { ok: true, state: { times, lockedUntil: undefined } } }