Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

feat(email): add implementation of email sending module - #14

Merged
akinwalexander merged 9 commits into
devfrom
feat/BE-003-email-verification
May 10, 2026
Merged

feat(email): add implementation of email sending module#14
akinwalexander merged 9 commits into
devfrom
feat/BE-003-email-verification

Conversation

@sage-ali

@sage-ali sage-ali commented May 10, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

This PR implements a centralized and resilient Email Sending Module, transitioning the application from a brittle, single-provider setup to a modular system that supports automatic provider fallback and self-healing background processing.

Key Improvements:

  • Modularization: Successfully relocated MailerModule configuration from AppModule into a dedicated EmailModule for better encapsulation.
  • Smart Provider Backup: Implemented logic to prioritize Resend SMTP (RESEND_SMTP_*) while automatically falling back to generic SMTP credentials to ensure 100% delivery uptime.
  • Self-Healing Delivery: Added a retry mechanism to background email jobs (3 attempts with exponential backoff) to handle transient provider glitches.
  • Observability: Centralized error handling across all email job processors to provide structured, human-readable logs (identifying Job ID and Recipient).

Related Issue

Fixes BE-003 Implement Email Sending Module

Type of Change

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation updates
  • style: Code style/formatting changes
  • refactor: Code refactoring
  • perf: Performance improvements
  • test: Test additions/updates
  • chore: Build process or tooling changes
  • ci: CI configuration changes
  • other:

How Has This Been Tested?

  • Unit tests
  • Integration tests
  • Manual tests (Verified email delivery)

Test Evidence

resend email test resend Evidence image

Checklist

  • My code follows the project's coding style
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • I have included a screenshot showing all tests passing
  • I have included documentation screenshots (if applicable)

Additional Notes

The MAIL_FROM logic was standardized to support the Name <email> format.

Summary by CodeRabbit

  • New Features

    • Added Resend SMTP configuration support with automatic fallback to standard SMTP settings.
    • Added automatic retry logic for email delivery (3 attempts with exponential backoff).
  • Bug Fixes

    • Improved error handling in email processing with enhanced failure logging and retry propagation.
  • Tests

    • Added email queue service test coverage.
    • Added integration test script for SMTP email delivery verification.

Review Change Stack

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR refactors the email system by centralizing SMTP configuration into a dedicated provider, consolidating error handling across email queue consumers with structured logging, adding retry/backoff resilience to the queue service, and providing comprehensive test coverage including a Resend SMTP smoke test script.

Changes

Email Configuration and Resilience Refactoring

Layer / File(s) Summary
Configuration Provider and Environment Variables
.env.example, config/mailer.config.ts
Added DB_NAME and Resend SMTP environment variables (RESEND_SMTP_HOST, RESEND_SMTP_PORT, RESEND_SMTP_USER, RESEND_SMTP_API_KEY, MAIL_FROM) to template. Created mailerConfig NestJS provider that reads SMTP settings with fallback defaults (e.g., onboarding@resend.dev for user, port 587, formatted from address).
Application Module Integration
src/app.module.ts, src/modules/email/email.module.ts
Removed inline MailerModule.forRootAsync configuration and HandlebarsAdapter import from AppModule. Updated both modules to load centralized mailerConfig provider via ConfigModule.forRoot. EmailModule now calls mailerConfig() to construct transport, defaults, and template settings.
Centralized Error Handling
src/modules/email/email.consumer.ts
Added handleFailure helper that logs structured error information (context, message, jobId, recipient) and rethrows errors. Updated all seven email job handlers (sendWelcomeEmailJob, sendWaitlistEmailJob, sendResetPasswordEmailJob, sendNewsletterEmailJob, sendTokenEmailJob, sendLoginOtpEmailJob, sendNotificationMail) to delegate error handling to this helper instead of inline logging.
Queue Service Retry Configuration
src/modules/email/queue.service.ts
Updated QueueService to use @Injectable() decorator syntax. Enhanced sendMail to enqueue jobs with retry configuration (attempts: 3 and exponential backoff with initial delay: 5000).
Testing and Validation
scripts/test-resend.ts, src/modules/email/queue.service.spec.ts, src/modules/email/email.consumer.spec.ts
Created Resend SMTP smoke test script that validates SMTP connectivity, sends test emails, and logs metadata. Added test suite for QueueService.sendMail verifying retry/backoff configuration and job enqueueing. Updated EmailQueueConsumer test to mock job with fixed id and verify error logging includes message, jobId, and recipient in structured format.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing an email sending module with centralized configuration, provider fallback, retry mechanisms, and error handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/BE-003-email-verification

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/mailer.config.ts`:
- Line 12: The current fallback for the mailer `from` (process.env.MAIL_FROM ??
`"FlowBrand" <${smtpUser}>`) can produce an invalid address when `smtpUser` is
not an email (e.g., "resend"); update the logic that builds the `from` field in
the mailer config so that it: checks process.env.MAIL_FROM first, else uses
`"FlowBrand" <${smtpUser}>` only if `smtpUser` contains an '@', and otherwise
falls back to a safe sender (e.g., `"FlowBrand"
<no-reply@${process.env.MAIL_DOMAIN ?? 'localhost'}>`); locate and change the
`from` assignment that references `process.env.MAIL_FROM` and `smtpUser` in
mailer.config.ts.
- Around line 8-11: The mailer config currently allows building an SMTP config
with missing credentials (host, user, pass) which defers failure; update the
config creation logic around host, port, user, pass (the variables assigned from
process.env.RESEND_SMTP_HOST/SMTP_HOST, smtpUser, and
process.env.RESEND_SMTP_API_KEY/SMTP_PASSWORD) to validate required values
immediately and throw a clear Error if host or password/user are missing so the
app fails fast during startup rather than later when jobs run.

In `@scripts/test-resend.ts`:
- Line 7: The port assignment using Number(...) can produce NaN for non-numeric
env values; update the code that sets the port (the const port variable in
scripts/test-resend.ts) to parse and validate the environment value(s)
(RESEND_SMTP_PORT, SMTP_PORT): attempt parseInt on the chosen string, check
Number.isFinite/Number.isInteger and that the value is within 1–65535, and if
invalid fall back to the default 587 (or log/throw a clear error); ensure the
validated numeric port is used when creating the nodemailer transport.
- Around line 26-30: The nodemailer transport created in
nodemailer.createTransport (const transporter) lacks an explicit secure flag
which breaks SMTPS on port 465; update the transport options to set secure: true
when port === 465 (or numeric 465) and secure: false for port 587 (or other
ports), e.g., compute a boolean from the port env and include secure in the
object passed to nodemailer.createTransport along with host/port/auth so the
transporter uses SMTPS for 465 and STARTTLS for 587.

In `@src/modules/email/email.consumer.ts`:
- Around line 13-18: The error log currently includes full recipient emails in
the logger.error call (see use of logger.error and job.data?.mail?.to); change
this to log a non-PII representation by replacing job.data?.mail?.to with a
masked or hashed value (e.g., maskEmail(recipient) or sha256(recipient)) before
calling logger.error, preserving other fields (context, error, jobId) and
ensuring the masking/hashing helper is used wherever recipient is logged in
email.consumer.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e3e44fd6-11fe-4876-9883-a8585d9250dd

📥 Commits

Reviewing files that changed from the base of the PR and between 0e25509 and 85f93d1.

📒 Files selected for processing (9)
  • .env.example
  • config/mailer.config.ts
  • scripts/test-resend.ts
  • src/app.module.ts
  • src/modules/email/email.consumer.spec.ts
  • src/modules/email/email.consumer.ts
  • src/modules/email/email.module.ts
  • src/modules/email/queue.service.spec.ts
  • src/modules/email/queue.service.ts

Comment thread config/mailer.config.ts Outdated
Comment thread config/mailer.config.ts Outdated
Comment thread scripts/test-resend.ts
dotenv.config();

const host = process.env.RESEND_SMTP_HOST ?? process.env.SMTP_HOST;
const port = Number(process.env.RESEND_SMTP_PORT ?? process.env.SMTP_PORT ?? 587);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate port number conversion to prevent NaN runtime errors.

If RESEND_SMTP_PORT or SMTP_PORT contains a non-numeric string, Number() will return NaN, which will cause nodemailer to fail when creating the transport. Add validation to ensure the port is a valid number.

🛡️ Proposed fix to validate port number
-const port = Number(process.env.RESEND_SMTP_PORT ?? process.env.SMTP_PORT ?? 587);
+const portEnv = process.env.RESEND_SMTP_PORT ?? process.env.SMTP_PORT ?? '587';
+const port = Number(portEnv);
+if (isNaN(port) || port <= 0 || port > 65535) {
+  console.error(`Invalid port number: ${portEnv}`);
+  process.exit(1);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const port = Number(process.env.RESEND_SMTP_PORT ?? process.env.SMTP_PORT ?? 587);
const portEnv = process.env.RESEND_SMTP_PORT ?? process.env.SMTP_PORT ?? '587';
const port = Number(portEnv);
if (isNaN(port) || port <= 0 || port > 65535) {
console.error(`Invalid port number: ${portEnv}`);
process.exit(1);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-resend.ts` at line 7, The port assignment using Number(...) can
produce NaN for non-numeric env values; update the code that sets the port (the
const port variable in scripts/test-resend.ts) to parse and validate the
environment value(s) (RESEND_SMTP_PORT, SMTP_PORT): attempt parseInt on the
chosen string, check Number.isFinite/Number.isInteger and that the value is
within 1–65535, and if invalid fall back to the default 587 (or log/throw a
clear error); ensure the validated numeric port is used when creating the
nodemailer transport.

Comment thread scripts/test-resend.ts
Comment on lines +26 to +30
const transporter = nodemailer.createTransport({
host,
port,
auth: { user, pass },
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check nodemailer documentation for secure option requirements

# Search for nodemailer SMTP transport configuration in the codebase
rg -n "createTransport" --type=ts -A 5 -B 2

Repository: hngprojects/flowbrand-be

Length of output: 444


🏁 Script executed:

#!/bin/bash
# Get more context around the transport creation in test-resend.ts
echo "=== Full test-resend.ts file context ==="
cat -n scripts/test-resend.ts | head -50

echo -e "\n=== Check for nodemailer version in package.json ==="
grep -i "nodemailer" package.json

echo -e "\n=== Search for other transport configurations in the codebase ==="
rg -n "createTransport" --type=ts,js -A 3

echo -e "\n=== Check for any secure option usage elsewhere ==="
rg -n "secure:" --type=ts,js -B 2 -A 1

Repository: hngprojects/flowbrand-be

Length of output: 2127


🏁 Script executed:

#!/bin/bash
# Check for package.json in root
echo "=== Checking for package.json ==="
find . -name "package.json" -type f | head -5

# Try to find package.json more directly
echo -e "\n=== Package.json content (looking for nodemailer) ==="
if [ -f "package.json" ]; then
  cat package.json | grep -A 2 -B 2 "nodemailer" || echo "nodemailer not found in package.json"
fi

# Look for node_modules or other clues about nodemailer version
echo -e "\n=== Search for any documentation about nodemailer ==="
rg -i "nodemailer" --type-add "docs:md,txt" -t docs -A 2 -B 2 || echo "No docs found"

# Look at environment variable usage for port/host
echo -e "\n=== Check how port variable is used ==="
rg -n "port" scripts/test-resend.ts

Repository: hngprojects/flowbrand-be

Length of output: 1215


🌐 Web query:

nodemailer createTransport secure option default behavior port 587 465

💡 Result:

In Nodemailer, the secure option for SMTP createTransport defaults to false [1]. - If secure: true, Nodemailer connects with TLS/SSL immediately. Use this for port 465 (SMTPS) [1]. - If secure: false (default), it connects unencrypted initially and upgrades to TLS via STARTTLS if the server supports it (automatic unless ignoreTLS: true) [1]. Use this for port 587 (submission) or 25 [2][1]. The port defaults to 587 if unspecified when secure: false, or 465 if secure: true [1]. Examples from official docs: - Port 587: { port: 587, secure: false } (STARTTLS) [2][3]. - Port 465: { port: 465, secure: true } (implicit TLS) [1][4].

Citations:


Add explicit secure option to transport configuration.

The transport configuration relies on nodemailer's default secure: false, which happens to work for port 587 (STARTTLS) but will fail for port 465 (SMTPS). Since the port is configurable via environment variables, this creates a fragility: if SMTP_PORT is set to 465, the connection will fail without the explicit secure option. Set secure: true for port 465 and secure: false for port 587 to match the protocol requirements.

🔒 Proposed fix
  const transporter = nodemailer.createTransport({
    host,
    port,
+   secure: port === 465,
    auth: { user, pass },
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const transporter = nodemailer.createTransport({
host,
port,
auth: { user, pass },
});
const transporter = nodemailer.createTransport({
host,
port,
secure: port === 465,
auth: { user, pass },
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-resend.ts` around lines 26 - 30, The nodemailer transport
created in nodemailer.createTransport (const transporter) lacks an explicit
secure flag which breaks SMTPS on port 465; update the transport options to set
secure: true when port === 465 (or numeric 465) and secure: false for port 587
(or other ports), e.g., compute a boolean from the port env and include secure
in the object passed to nodemailer.createTransport along with host/port/auth so
the transporter uses SMTPS for 465 and STARTTLS for 587.

Comment thread src/modules/email/email.consumer.ts

@Nuel-09 Nuel-09 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved

@ibraheembello
ibraheembello self-requested a review May 10, 2026 07:53
@Nuel-09
Nuel-09 self-requested a review May 10, 2026 07:58
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants