From d95ed9cd8d3a88b7fa4bb866ac96e702fa7b5180 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Thu, 9 Apr 2026 14:40:19 -0400 Subject: [PATCH] Switch contact email to Resend API, remove nodemailer/SMTP --- .env.example | 20 ++++++---- package-lock.json | 96 ++++++++++++++++++++++++++++++++++++----------- package.json | 5 +-- server.js | 32 +++++----------- 4 files changed, 98 insertions(+), 55 deletions(-) diff --git a/.env.example b/.env.example index b8df7e7..a501a0f 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,16 @@ # Copy this file to .env and fill in your real values. # Never commit .env to version control. -# Outlook SMTP credentials for the contact form -# Use an app password (not your main password): -# 1. Go to account.microsoft.com → Security → Advanced security options -# 2. Enable 2-step verification if not already on -# 3. Create an App password and paste it here -SMTP_USER=vbvwithnate@outlook.com -SMTP_PASS=your_app_password_here +# Resend API key for the contact form +# 1. Sign up free at https://resend.com +# 2. Go to API Keys and create a new key +# 3. Paste it below +RESEND_API_KEY=re_your_api_key_here + +# Email address that receives contact form submissions +RESEND_TO=vbvwithnate@outlook.com + +# "From" address shown on received emails. +# During testing you can leave this as-is (uses Resend's shared domain). +# For production: verify your own domain at resend.com/domains and change this. +RESEND_FROM=Verse by Verse diff --git a/package-lock.json b/package-lock.json index 0e207cd..20fdf2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,17 +9,16 @@ "version": "0.0.0", "dependencies": { "express": "^5.2.1", - "nodemailer": "^8.0.5", "react": "^19.2.4", "react-dom": "^19.2.4", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.1", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "resend": "^6.10.0" }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^24.12.0", - "@types/nodemailer": "^8.0.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.0", @@ -865,6 +864,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -941,16 +946,6 @@ "undici-types": "~7.16.0" } }, - "node_modules/@types/nodemailer": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", - "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -2251,6 +2246,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4141,15 +4142,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nodemailer": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", - "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4330,6 +4322,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -4603,6 +4601,27 @@ "node": ">=0.10.0" } }, + "node_modules/resend": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.10.0.tgz", + "integrity": "sha512-i7CwZpYj4Oho1RxsTpLcCUkO08+HiL4NXrm6jLJ2WzJ89UGI8eROSieLONJA3hnUrf1OYnCyfq5F6POnHUMv1Q==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "svix": "1.88.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4887,6 +4906,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4982,6 +5011,16 @@ "node": ">=8" } }, + "node_modules/svix": { + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.88.0.tgz", + "integrity": "sha512-vm/JrrUd3bVyBE+3L33TIyVSs8gS5fYx7lrISvKlDJXTYX1ACH4REX8P1tHxsSKoZi/rvifM1t0XRc5Vc45THw==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -5267,6 +5306,19 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 53c4eb0..667dba6 100644 --- a/package.json +++ b/package.json @@ -14,17 +14,16 @@ }, "dependencies": { "express": "^5.2.1", - "nodemailer": "^8.0.5", "react": "^19.2.4", "react-dom": "^19.2.4", "react-markdown": "^10.1.0", "react-router-dom": "^7.13.1", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "resend": "^6.10.0" }, "devDependencies": { "@eslint/js": "^9.39.4", "@types/node": "^24.12.0", - "@types/nodemailer": "^8.0.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.0", diff --git a/server.js b/server.js index 86e9f96..047c6de 100644 --- a/server.js +++ b/server.js @@ -2,15 +2,7 @@ import express from 'express' import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' -import nodemailer from 'nodemailer' - -function htmlEsc(str) { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} +import { Resend } from 'resend' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -97,27 +89,21 @@ app.post('/api/contact', contactRateLimit, async (req, res) => { return } - if (!process.env.SMTP_USER || !process.env.SMTP_PASS) { - console.error('[contact] SMTP_USER / SMTP_PASS env vars not set') + if (!process.env.RESEND_API_KEY) { + console.error('[contact] RESEND_API_KEY env var not set') res.status(503).json({ message: 'The contact form is not yet configured on the server.' }) return } - const transporter = nodemailer.createTransport({ - host: 'smtp-mail.outlook.com', - port: 587, - secure: false, - auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }, - }) - - await transporter.sendMail({ - from: process.env.SMTP_USER, - to: process.env.SMTP_USER, - replyTo: `"${name.trim()}" <${email.trim()}>`, + const resend = new Resend(process.env.RESEND_API_KEY) + const { error } = await resend.emails.send({ + from: process.env.RESEND_FROM ?? 'Verse by Verse ', + to: [process.env.RESEND_TO ?? 'vbvwithnate@outlook.com'], + reply_to: `${name.trim()} <${email.trim()}>`, subject: `New message from Verse by Verse website — ${name.trim()}`, text: `Name: ${name.trim()}\nEmail: ${email.trim()}\n\nMessage:\n${message.trim()}`, - html: `

Name: ${htmlEsc(name.trim())}

Email: ${htmlEsc(email.trim())}


${htmlEsc(message.trim())}
`, }) + if (error) throw error res.json({ ok: true }) } catch (err) {