From 1d95d1f398cdf3f9e1e6c6d7a2c905ffc8392870 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 15 Feb 2026 16:13:05 -0700 Subject: [PATCH] Replace Resend with self-hosted email relay for all email sending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cryptidAuth.ts: sendEmail() now calls email-relay.jeffemmett.com instead of api.resend.com - boardPermissions.ts: admin request emails use email relay - types.ts: RESEND_API_KEY → EMAIL_RELAY_URL + EMAIL_RELAY_API_KEY - wrangler.toml: updated secrets documentation - Tests updated with new mock env vars Email relay is a lightweight Flask service on Netcup that accepts HTTP POST and sends via Mailcow SMTP. Needed because CF Workers can't do TCP/SMTP directly. Co-Authored-By: Claude Opus 4.6 --- tests/worker/board-permissions.test.ts | 3 ++- tests/worker/cryptid-auth.test.ts | 3 ++- worker/boardPermissions.ts | 6 +++--- worker/cryptidAuth.ts | 14 ++++++++------ worker/types.ts | 3 ++- wrangler.toml | 2 +- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/worker/board-permissions.test.ts b/tests/worker/board-permissions.test.ts index f633356..4ae9136 100644 --- a/tests/worker/board-permissions.test.ts +++ b/tests/worker/board-permissions.test.ts @@ -75,7 +75,8 @@ function createMockEnv(overrides: Partial = {}): Environment { DAILY_API_KEY: 'mock-daily-key', DAILY_DOMAIN: 'mock.daily.co', CRYPTID_DB: createMockD1() as unknown as D1Database, - RESEND_API_KEY: 'mock-resend-key', + EMAIL_RELAY_URL: 'https://email-relay.jeffemmett.com', + EMAIL_RELAY_API_KEY: 'mock-relay-key', APP_URL: 'https://test.example.com', ...overrides, } diff --git a/tests/worker/cryptid-auth.test.ts b/tests/worker/cryptid-auth.test.ts index d30f7b0..4ffe312 100644 --- a/tests/worker/cryptid-auth.test.ts +++ b/tests/worker/cryptid-auth.test.ts @@ -58,7 +58,8 @@ function createMockEnv(overrides: Partial = {}): Environment { DAILY_API_KEY: 'mock-daily-key', DAILY_DOMAIN: 'mock.daily.co', CRYPTID_DB: createMockD1() as unknown as D1Database, - RESEND_API_KEY: 'mock-resend-key', + EMAIL_RELAY_URL: 'https://email-relay.jeffemmett.com', + EMAIL_RELAY_API_KEY: 'mock-relay-key', APP_URL: 'https://test.example.com', ...overrides, } diff --git a/worker/boardPermissions.ts b/worker/boardPermissions.ts index dd1165e..c7ff523 100644 --- a/worker/boardPermissions.ts +++ b/worker/boardPermissions.ts @@ -1099,13 +1099,13 @@ export async function handleRequestAdminAccess( const body = await request.json().catch(() => ({})) as { reason?: string }; // Send email to global admin (jeffemmett@gmail.com) - if (env.RESEND_API_KEY) { + if (env.EMAIL_RELAY_URL && env.EMAIL_RELAY_API_KEY) { const emailFrom = env.CRYPTID_EMAIL_FROM || 'Canvas '; - const emailResponse = await fetch('https://api.resend.com/emails', { + const emailResponse = await fetch(`${env.EMAIL_RELAY_URL}/send`, { method: 'POST', headers: { - 'Authorization': `Bearer ${env.RESEND_API_KEY}`, + 'Authorization': `Bearer ${env.EMAIL_RELAY_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ diff --git a/worker/cryptidAuth.ts b/worker/cryptidAuth.ts index eaa346e..b73223d 100644 --- a/worker/cryptidAuth.ts +++ b/worker/cryptidAuth.ts @@ -12,7 +12,7 @@ function generateUUID(): string { return crypto.randomUUID(); } -// Send email via Resend +// Send email via self-hosted email relay (Mailcow SMTP) async function sendEmail( env: Environment, to: string, @@ -20,15 +20,17 @@ async function sendEmail( htmlContent: string ): Promise { try { - if (!env.RESEND_API_KEY) { - console.error('RESEND_API_KEY not configured'); + const relayUrl = env.EMAIL_RELAY_URL; + const relayKey = env.EMAIL_RELAY_API_KEY; + if (!relayUrl || !relayKey) { + console.error('EMAIL_RELAY_URL or EMAIL_RELAY_API_KEY not configured'); return false; } - const response = await fetch('https://api.resend.com/emails', { + const response = await fetch(`${relayUrl}/send`, { method: 'POST', headers: { - 'Authorization': `Bearer ${env.RESEND_API_KEY}`, + 'Authorization': `Bearer ${relayKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ @@ -41,7 +43,7 @@ async function sendEmail( if (!response.ok) { const errorText = await response.text(); - console.error('Resend error:', errorText); + console.error('Email relay error:', errorText); return false; } diff --git a/worker/types.ts b/worker/types.ts index 3132d8b..89094d8 100644 --- a/worker/types.ts +++ b/worker/types.ts @@ -10,7 +10,8 @@ export interface Environment { DAILY_DOMAIN: string; // CryptID auth bindings CRYPTID_DB?: D1Database; - RESEND_API_KEY?: string; + EMAIL_RELAY_URL?: string; + EMAIL_RELAY_API_KEY?: string; CRYPTID_EMAIL_FROM?: string; APP_URL?: string; // Admin secret for protected endpoints diff --git a/wrangler.toml b/wrangler.toml index 06cedde..992efa8 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -108,7 +108,7 @@ crons = ["0 0 * * *"] # Run at midnight UTC every day # - CLOUDFLARE_API_TOKEN # - FAL_API_KEY # For fal.ai image/video generation proxy # - RUNPOD_API_KEY # For RunPod AI endpoints proxy -# - RESEND_API_KEY # For email sending +# - EMAIL_RELAY_API_KEY # For email sending via self-hosted relay # - ADMIN_SECRET # For admin-only endpoints # # To set secrets: