From 620bc229ed702cb9594c16a58e0516d28fb881dd Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 15 Feb 2026 09:48:53 -0700 Subject: [PATCH] feat: replace Resend SDK with Mailcow SMTP (nodemailer) Swap email sending from Resend API to self-hosted Mailcow at mx.jeffemmett.com. Uses SMTP_PASS env var instead of RESEND_API_KEY. Co-Authored-By: Claude Opus 4.6 --- api/application.js | 32 ++++++++++++++++++++------------ api/waitlist-db.js | 24 ++++++++++++++++-------- package.json | 4 ++-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/api/application.js b/api/application.js index f6c3b95..aaf4f68 100644 --- a/api/application.js +++ b/api/application.js @@ -1,8 +1,8 @@ // Application form API endpoint -// Handles full event applications with PostgreSQL storage and Resend emails +// Handles full event applications with PostgreSQL storage and Mailcow SMTP emails const { Pool } = require('pg'); -const { Resend } = require('resend'); +const nodemailer = require('nodemailer'); // Initialize PostgreSQL connection pool const pool = new Pool({ @@ -10,8 +10,16 @@ const pool = new Pool({ ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false }); -// Initialize Resend -const resend = new Resend(process.env.RESEND_API_KEY); +// Initialize SMTP transport (Mailcow) +const smtp = nodemailer.createTransport({ + host: process.env.SMTP_HOST || 'mx.jeffemmett.com', + port: parseInt(process.env.SMTP_PORT || '587'), + secure: false, + auth: { + user: process.env.SMTP_USER || 'noreply@jeffemmett.com', + pass: process.env.SMTP_PASS || '', + }, +}); // Email templates const confirmationEmail = (application) => ({ @@ -238,17 +246,17 @@ module.exports = async function handler(req, res) { }; // Send confirmation email to applicant - if (process.env.RESEND_API_KEY) { + if (process.env.SMTP_PASS) { try { const confirmEmail = confirmationEmail(application); - const { data: emailData } = await resend.emails.send({ + const info = await smtp.sendMail({ from: process.env.EMAIL_FROM || 'Valley of the Commons ', to: application.email, subject: confirmEmail.subject, - html: confirmEmail.html + html: confirmEmail.html, }); await logEmail(application.email, `${application.first_name} ${application.last_name}`, - 'application_confirmation', confirmEmail.subject, emailData?.id, { applicationId: application.id }); + 'application_confirmation', confirmEmail.subject, info.messageId, { applicationId: application.id }); } catch (emailError) { console.error('Failed to send confirmation email:', emailError); } @@ -257,14 +265,14 @@ module.exports = async function handler(req, res) { try { const adminEmail = adminNotificationEmail(application); const adminRecipients = (process.env.ADMIN_EMAILS || 'jeff@jeffemmett.com').split(','); - const { data: emailData } = await resend.emails.send({ + const info = await smtp.sendMail({ from: process.env.EMAIL_FROM || 'Valley of the Commons ', - to: adminRecipients, + to: adminRecipients.join(', '), subject: adminEmail.subject, - html: adminEmail.html + html: adminEmail.html, }); await logEmail(adminRecipients[0], 'Admin', 'admin_notification', - adminEmail.subject, emailData?.id, { applicationId: application.id }); + adminEmail.subject, info.messageId, { applicationId: application.id }); } catch (emailError) { console.error('Failed to send admin notification:', emailError); } diff --git a/api/waitlist-db.js b/api/waitlist-db.js index 0283f79..fbfa008 100644 --- a/api/waitlist-db.js +++ b/api/waitlist-db.js @@ -1,8 +1,8 @@ // Waitlist API endpoint using PostgreSQL -// Simple interest signups with email confirmation via Resend +// Simple interest signups with email confirmation via Mailcow SMTP const { Pool } = require('pg'); -const { Resend } = require('resend'); +const nodemailer = require('nodemailer'); // Initialize PostgreSQL connection pool const pool = new Pool({ @@ -10,8 +10,16 @@ const pool = new Pool({ ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false }); -// Initialize Resend -const resend = new Resend(process.env.RESEND_API_KEY); +// Initialize SMTP transport (Mailcow) +const smtp = nodemailer.createTransport({ + host: process.env.SMTP_HOST || 'mx.jeffemmett.com', + port: parseInt(process.env.SMTP_PORT || '587'), + secure: false, + auth: { + user: process.env.SMTP_USER || 'noreply@jeffemmett.com', + pass: process.env.SMTP_PASS || '', + }, +}); const welcomeEmail = (signup) => ({ subject: 'Welcome to Valley of the Commons', @@ -137,16 +145,16 @@ module.exports = async function handler(req, res) { }; // Send welcome email - if (process.env.RESEND_API_KEY) { + if (process.env.SMTP_PASS) { try { const email = welcomeEmail(signup); - const { data: emailData } = await resend.emails.send({ + const info = await smtp.sendMail({ from: process.env.EMAIL_FROM || 'Valley of the Commons ', to: signup.email, subject: email.subject, - html: email.html + html: email.html, }); - await logEmail(signup.email, signup.name, 'waitlist_welcome', email.subject, emailData?.id); + await logEmail(signup.email, signup.name, 'waitlist_welcome', email.subject, info.messageId); } catch (emailError) { console.error('Failed to send welcome email:', emailError); // Don't fail the request if email fails diff --git a/package.json b/package.json index 7d1f947..de659be 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "@octokit/rest": "^22.0.1", "ai": "^6.0.1", "googleapis": "^126.0.1", - "pg": "^8.13.0", - "resend": "^4.0.0" + "nodemailer": "^6.9.0", + "pg": "^8.13.0" }, "devDependencies": { "dotenv": "^16.3.1"