From 7121b57cd7b0b8de8e757ffb658621f8a0658120 Mon Sep 17 00:00:00 2001 From: keshav-005 Date: Wed, 22 Apr 2026 20:25:16 +0530 Subject: [PATCH 1/3] Make email configuration optional --- README.md | 8 +++--- lib/nodemailer/index.ts | 58 ++++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 018a9988..24ca2894 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Prerequisites - Node.js 20+ and pnpm or npm - MongoDB connection string (MongoDB Atlas or local via Docker Compose) - Finnhub API key (free tier supported; real-time may require paid) -- Gmail account for email (or update Nodemailer transport) +- Optional: Gmail account for email (or update Nodemailer transport) if you want welcome and news summary emails - Optional: Google Gemini API key (for AI-generated welcome intros) Clone and install @@ -208,6 +208,7 @@ Notes - The app service depends_on the mongodb service. - Credentials are defined in Compose for the MongoDB root user; authSource=admin is required on the connection string for root. - Data persists across restarts via the docker volume. +- `NODEMAILER_EMAIL` and `NODEMAILER_PASSWORD` are optional for local Docker runs. If they are omitted, the app still starts but email features stay disabled. Optional: Example MongoDB service definition used in this project: ```yaml @@ -273,7 +274,7 @@ GEMINI_API_KEY=your_gemini_api_key # Get this from your Inngest dashboard: https://app.inngest.com/env/settings/keys INNGEST_SIGNING_KEY=your_inngest_signing_key -# Email (Nodemailer via Gmail; consider App Passwords if 2FA) +# Email (optional; Nodemailer via Gmail, consider App Passwords if 2FA) NODEMAILER_EMAIL=youraddress@gmail.com NODEMAILER_PASSWORD=your_gmail_app_password ``` @@ -314,7 +315,7 @@ GEMINI_API_KEY=your_gemini_api_key # Get this from your Inngest dashboard: https://app.inngest.com/env/settings/keys INNGEST_SIGNING_KEY=your_inngest_signing_key -# Email (Nodemailer via Gmail; consider App Passwords if 2FA) +# Email (optional; Nodemailer via Gmail, consider App Passwords if 2FA) NODEMAILER_EMAIL=youraddress@gmail.com NODEMAILER_PASSWORD=your_gmail_app_password ``` @@ -323,6 +324,7 @@ Notes - Keep private keys server-side whenever possible. - If using `NEXT_PUBLIC_` variables, remember they are exposed to the browser. - In production, prefer a dedicated SMTP provider over a personal Gmail. +- If the Nodemailer credentials are omitted, the app still runs but welcome and news summary emails are disabled. - Do not hardcode secrets in the Dockerfile; use `.env` and Compose. ## 🧱 Project Structure diff --git a/lib/nodemailer/index.ts b/lib/nodemailer/index.ts index 215722b4..17505534 100644 --- a/lib/nodemailer/index.ts +++ b/lib/nodemailer/index.ts @@ -1,35 +1,39 @@ import nodemailer from 'nodemailer'; -import {WELCOME_EMAIL_TEMPLATE, NEWS_SUMMARY_EMAIL_TEMPLATE} from "@/lib/nodemailer/templates"; +import { WELCOME_EMAIL_TEMPLATE, NEWS_SUMMARY_EMAIL_TEMPLATE } from "@/lib/nodemailer/templates"; -// Verify transporter configuration -if (!process.env.NODEMAILER_EMAIL || !process.env.NODEMAILER_PASSWORD) { - console.warn('⚠️ NODEMAILER_EMAIL or NODEMAILER_PASSWORD is not set. Email functionality will not work.'); +const hasEmailConfig = Boolean(process.env.NODEMAILER_EMAIL && process.env.NODEMAILER_PASSWORD); + +if (!hasEmailConfig) { + console.warn('⚠️ Email credentials are not configured. Welcome and news summary emails are disabled until NODEMAILER_EMAIL and NODEMAILER_PASSWORD are set.'); } -export const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - user: process.env.NODEMAILER_EMAIL!, - pass: process.env.NODEMAILER_PASSWORD!, - }, - // Add connection timeout and retry options - pool: true, - maxConnections: 1, - maxMessages: 3, -}) +export const transporter = hasEmailConfig + ? nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.NODEMAILER_EMAIL!, + pass: process.env.NODEMAILER_PASSWORD!, + }, + // Keep the pool small because email volume is low in this app. + pool: true, + maxConnections: 1, + maxMessages: 3, + }) + : null; -// Verify connection on startup -transporter.verify((error, success) => { - if (error) { - console.error('❌ Nodemailer transporter verification failed:', error); - } else { - console.log('✅ Nodemailer transporter is ready to send emails'); - } -}); +if (transporter) { + transporter.verify((error) => { + if (error) { + console.error('❌ Nodemailer transporter verification failed:', error); + } else { + console.log('✅ Nodemailer transporter is ready to send emails'); + } + }); +} export const sendWelcomeEmail = async ({ email, name, intro }: WelcomeEmailData) => { try { - if (!process.env.NODEMAILER_EMAIL || !process.env.NODEMAILER_PASSWORD) { + if (!transporter) { throw new Error('Email credentials not configured'); } @@ -43,7 +47,7 @@ export const sendWelcomeEmail = async ({ email, name, intro }: WelcomeEmailData) subject: `Welcome to Openstock - your open-source stock market toolkit!`, text: 'Thanks for joining Openstock, an initiative by open dev society', html: htmlTemplate, - } + }; const info = await transporter.sendMail(mailOptions); console.log('✅ Welcome email sent successfully:', info.messageId); @@ -58,7 +62,7 @@ export const sendNewsSummaryEmail = async ( { email, date, newsContent }: { email: string; date: string; newsContent: string } ) => { try { - if (!process.env.NODEMAILER_EMAIL || !process.env.NODEMAILER_PASSWORD) { + if (!transporter) { throw new Error('Email credentials not configured'); } @@ -81,4 +85,4 @@ export const sendNewsSummaryEmail = async ( console.error('❌ Failed to send news summary email:', error); throw error; } -}; \ No newline at end of file +}; From 04b5a27bf9ded2b74e2be0bb5e7a65425f1b7c22 Mon Sep 17 00:00:00 2001 From: keshav-005 Date: Wed, 22 Apr 2026 20:53:50 +0530 Subject: [PATCH 2/3] Skip disabled email sends cleanly --- README.md | 8 ++++---- lib/nodemailer/index.ts | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 24ca2894..3175212b 100644 --- a/README.md +++ b/README.md @@ -275,8 +275,8 @@ GEMINI_API_KEY=your_gemini_api_key INNGEST_SIGNING_KEY=your_inngest_signing_key # Email (optional; Nodemailer via Gmail, consider App Passwords if 2FA) -NODEMAILER_EMAIL=youraddress@gmail.com -NODEMAILER_PASSWORD=your_gmail_app_password +# NODEMAILER_EMAIL=youraddress@gmail.com +# NODEMAILER_PASSWORD=your_gmail_app_password ``` Local (Docker Compose) MongoDB: @@ -316,8 +316,8 @@ GEMINI_API_KEY=your_gemini_api_key INNGEST_SIGNING_KEY=your_inngest_signing_key # Email (optional; Nodemailer via Gmail, consider App Passwords if 2FA) -NODEMAILER_EMAIL=youraddress@gmail.com -NODEMAILER_PASSWORD=your_gmail_app_password +# NODEMAILER_EMAIL=youraddress@gmail.com +# NODEMAILER_PASSWORD=your_gmail_app_password ``` Notes diff --git a/lib/nodemailer/index.ts b/lib/nodemailer/index.ts index 17505534..bfbae5c6 100644 --- a/lib/nodemailer/index.ts +++ b/lib/nodemailer/index.ts @@ -34,7 +34,8 @@ if (transporter) { export const sendWelcomeEmail = async ({ email, name, intro }: WelcomeEmailData) => { try { if (!transporter) { - throw new Error('Email credentials not configured'); + console.warn('⚠️ Welcome email skipped: email credentials are not configured.'); + return null; } const htmlTemplate = WELCOME_EMAIL_TEMPLATE @@ -63,7 +64,8 @@ export const sendNewsSummaryEmail = async ( ) => { try { if (!transporter) { - throw new Error('Email credentials not configured'); + console.warn('⚠️ News summary email skipped: email credentials are not configured.'); + return null; } const htmlTemplate = NEWS_SUMMARY_EMAIL_TEMPLATE From 101411d9d66a5f0e64ff03b6cfec419ec017ef7b Mon Sep 17 00:00:00 2001 From: keshav-005 Date: Wed, 22 Apr 2026 21:03:55 +0530 Subject: [PATCH 3/3] Differentiate skipped welcome emails --- lib/inngest/functions.ts | 15 +++++++++------ lib/nodemailer/index.ts | 16 ++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/inngest/functions.ts b/lib/inngest/functions.ts index e4e47dcf..48e5a2ba 100644 --- a/lib/inngest/functions.ts +++ b/lib/inngest/functions.ts @@ -29,7 +29,7 @@ export const sendSignUpEmail = inngest.createFunction( } }); - await step.run('send-welcome-email', async () => { + const emailResult = await step.run('send-welcome-email', async () => { try { const { data: { email, name } } = event; @@ -37,6 +37,10 @@ export const sendSignUpEmail = inngest.createFunction( console.log(`📧 Attempting to send welcome email to: ${email}`); const result = await sendWelcomeEmail({ email, name, intro: introText }); + if (result.status !== 'sent') { + console.log(`Welcome email skipped for: ${email}`); + return result; + } console.log(`✅ Welcome email sent successfully to: ${email}`); return result; } catch (error) { @@ -45,10 +49,9 @@ export const sendSignUpEmail = inngest.createFunction( } }) - return { - success: true, - message: 'Welcome email sent successfully' - } + return emailResult.status === 'sent' + ? { success: true, message: 'Welcome email sent successfully' } + : { success: true, message: 'Welcome email skipped because email credentials are not configured' }; } ) @@ -463,4 +466,4 @@ export const checkInactiveUsers = inngest.createFunction( return { processed: inactiveUsers.length, sent: results }; } -); \ No newline at end of file +); diff --git a/lib/nodemailer/index.ts b/lib/nodemailer/index.ts index bfbae5c6..19f46fa5 100644 --- a/lib/nodemailer/index.ts +++ b/lib/nodemailer/index.ts @@ -1,6 +1,10 @@ import nodemailer from 'nodemailer'; import { WELCOME_EMAIL_TEMPLATE, NEWS_SUMMARY_EMAIL_TEMPLATE } from "@/lib/nodemailer/templates"; +type EmailSendResult = + | { status: 'skipped' } + | { status: 'sent'; messageId: string }; + const hasEmailConfig = Boolean(process.env.NODEMAILER_EMAIL && process.env.NODEMAILER_PASSWORD); if (!hasEmailConfig) { @@ -34,8 +38,8 @@ if (transporter) { export const sendWelcomeEmail = async ({ email, name, intro }: WelcomeEmailData) => { try { if (!transporter) { - console.warn('⚠️ Welcome email skipped: email credentials are not configured.'); - return null; + console.warn('⚠️ Welcome email skipped: email credentials are not configured.'); + return { status: 'skipped' } satisfies EmailSendResult; } const htmlTemplate = WELCOME_EMAIL_TEMPLATE @@ -52,7 +56,7 @@ export const sendWelcomeEmail = async ({ email, name, intro }: WelcomeEmailData) const info = await transporter.sendMail(mailOptions); console.log('✅ Welcome email sent successfully:', info.messageId); - return info; + return { status: 'sent', messageId: info.messageId } satisfies EmailSendResult; } catch (error) { console.error('❌ Failed to send welcome email:', error); throw error; @@ -64,8 +68,8 @@ export const sendNewsSummaryEmail = async ( ) => { try { if (!transporter) { - console.warn('⚠️ News summary email skipped: email credentials are not configured.'); - return null; + console.warn('⚠️ News summary email skipped: email credentials are not configured.'); + return { status: 'skipped' } satisfies EmailSendResult; } const htmlTemplate = NEWS_SUMMARY_EMAIL_TEMPLATE @@ -82,7 +86,7 @@ export const sendNewsSummaryEmail = async ( const info = await transporter.sendMail(mailOptions); console.log('✅ News summary email sent successfully:', info.messageId); - return info; + return { status: 'sent', messageId: info.messageId } satisfies EmailSendResult; } catch (error) { console.error('❌ Failed to send news summary email:', error); throw error;