Skip to content

implement email service with zod and nodemailer#13

Open
Elad-Abutbul wants to merge 1 commit into
mainfrom
86extdhtj-emailService
Open

implement email service with zod and nodemailer#13
Elad-Abutbul wants to merge 1 commit into
mainfrom
86extdhtj-emailService

Conversation

@Elad-Abutbul
Copy link
Copy Markdown
Collaborator

@Elad-Abutbul Elad-Abutbul commented Jun 3, 2026

Description

Please include a summary of the changes and the related issue.

Related Issue(s)

Implemented a generic backend email service that validates requests with Zod, routes by emailType, builds the attendance email content, and sends it with Nodemailer.

The email content was separated into helpers/constants so the service stays clean and future email types can be added easily.

For Gmail, SMTP_PASS must be a Google App Password, and HTML values are escaped to prevent injected HTML/JS from being rendered.

Fixes # (issue number)

Checklist:

  • I have performed a self-review of my own code
  • My changes generate no new warnings

Screenshots (if appropriate):

image image

Summary by CodeRabbit

Release Notes

  • New Features

    • Email notification system for sending communications to users
    • Support for multiple email types with extensible architecture
    • Built-in validation of email data
    • HTML email templates with customizable styling
    • Comprehensive error handling and status messages
  • Chores

    • Added nodemailer and Zod package dependencies

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request introduces a complete email notification feature for attendance updates. Dependencies are added (nodemailer, zod, type definitions), constants define email types and Hebrew content, the service layer handles Zod validation and Nodemailer dispatch, helpers build attendance email HTML/text, and the HTTP layer exposes the feature via a POST endpoint with Swagger documentation.

Changes

Email Notification Feature

Layer / File(s) Summary
Email Constants & Configuration
package.json, src/constants/email/email.constants.ts
Dependencies added: nodemailer, zod, and type definitions. Email constants exported: EMAIL_TYPE (attendance update), ATTENDANCE_STATUS (coming/not-coming), Hebrew EMAIL_SUBJECTS and EMAIL_TEXT templates, error/success messages, and header colors for styling.
Email Service Core Logic
src/services/email/email.service.ts
Zod discriminated union schema validates email input by type. Nodemailer transporter configured for Gmail with environment credentials. sendEmail function validates payload, checks SMTP vars, builds content, dispatches, and maps errors to specific exception messages.
Email Content Builders
src/services/email/email.service.helpers.ts
AttendanceEmailPayload and EmailContent interfaces define input/output shapes. Helpers escape HTML, derive subject/text by attendance status, build plain-text message with optional reschedule line, and render RTL HTML table email with status-specific header colors and data rows.
Email Controller & Routes
src/controllers/email/email.controller.ts, src/routes/email.routes.ts
Controller calls service with req.body, returns HTTP 200 on success or HTTP 400/500 on error with mapped messages. Routes module documents POST /api/email endpoint via Swagger JSDoc, wires router.post('/', sendEmail), and exports the configured router.
App Integration
src/app.ts
Email routes imported and registered as middleware at /api/email path alongside existing patient/login/appointment/tips/metrics routes.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Controller
  participant Service
  participant Nodemailer
  participant Gmail
  Client->>Controller: POST /api/email {emailType, payload}
  Controller->>Service: sendEmail(data)
  Service->>Service: Validate with Zod schema
  Service->>Service: Check SMTP env vars
  Service->>Service: Build email content
  Service->>Nodemailer: transporter.sendMail({to, subject, text, html})
  Nodemailer->>Gmail: SMTP send
  Gmail-->>Nodemailer: Success
  Nodemailer-->>Service: Promise resolve
  Service-->>Controller: Promise resolve
  Controller-->>Client: 200 OK {message: emailSentSuccessfully}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops through the mail, with Zod so true,
Nodemailer whispers what's overdue.
Hebrew templates in RTL grace,
Attendance updates reach their place! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main implementation: adding an email service using Zod validation and Nodemailer for sending.
Description check ✅ Passed The description covers the implementation details, design decisions, and security considerations, though it lacks a specific issue number reference in the 'Fixes' section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

✏️ 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 86extdhtj-emailService

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
package.json (1)

24-24: Remove redundant @types/nodemailer: nodemailer ships its own typings.

nodemailer@8.0.10 exposes TypeScript types via index.d.ts, so @types/nodemailer is likely unnecessary and can be dropped to avoid redundancy/version drift for fresh installs and lockfile updates.

🤖 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 `@package.json` at line 24, Remove the redundant dev dependency entry
"`@types/nodemailer`" from package.json; nodemailer (>=8.0.10) includes its own
TypeScript types, so delete the "`@types/nodemailer`" dependency line and run the
package manager's install/update to refresh the lockfile (e.g., npm/yarn/pnpm
install) to ensure no stale typings remain.
🤖 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 `@src/constants/email/email.constants.ts`:
- Around line 23-24: The file has formatting issues for the exported constant
rescheduleMessage (and another at the same file around line 56); run Prettier or
reformat the string literals so they are wrapped consistently and ensure the
file ends with a single trailing newline, then re-run lint/format checks;
specifically update the rescheduleMessage value in email.constants.ts to match
project Prettier rules (line-wrapping/quotes) and add the missing final newline.

In `@src/routes/email.routes.ts`:
- Line 97: The POST /email route (router.post('/', sendEmail)) is unprotected;
wrap this route with the same authentication and staff-authorization middleware
used for staff-only actions (apply the existing authenticate and authorizeStaff
middleware functions in the same order as other protected routes) so only
authorized staff can send mail, and ensure the route handler still receives the
request after middleware. Also update the Swagger/OpenAPI docs for this endpoint
to include 401 and 403 responses and any required security schemes so clients
know authentication/authorization is required.

In `@src/services/email/email.service.ts`:
- Around line 23-29: The Nodemailer transporter created in createTransport lacks
SMTP timeouts causing sendMail (which runs on the request path) to potentially
hang; update the transporter creation (the const transporter / createTransport
call in email.service.ts) to include connectionTimeout, greetingTimeout, and
socketTimeout (e.g., read from env vars like SMTP_CONN_TIMEOUT,
SMTP_GREETING_TIMEOUT, SMTP_SOCKET_TIMEOUT or use sensible millisecond defaults)
so the SMTP client fails fast when Gmail is slow/unreachable.

---

Nitpick comments:
In `@package.json`:
- Line 24: Remove the redundant dev dependency entry "`@types/nodemailer`" from
package.json; nodemailer (>=8.0.10) includes its own TypeScript types, so delete
the "`@types/nodemailer`" dependency line and run the package manager's
install/update to refresh the lockfile (e.g., npm/yarn/pnpm install) to ensure
no stale typings remain.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: f13fa9dc-f178-401e-98ea-346451f44642

📥 Commits

Reviewing files that changed from the base of the PR and between a3180f2 and 561039d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • package.json
  • src/app.ts
  • src/constants/email/email.constants.ts
  • src/controllers/email/email.controller.ts
  • src/routes/email.routes.ts
  • src/services/email/email.service.helpers.ts
  • src/services/email/email.service.ts

Comment on lines +23 to +24
rescheduleMessage:
'אנא פנה למטופל לקביעת מועד חדש ועדכן במערכת כי התפנה כיסא.',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run Prettier on this file before merge.

These spots already trip prettier/prettier, so lint will stay red until the wrapped string and trailing newline are normalized.

Also applies to: 56-56

🧰 Tools
🪛 ESLint

[error] 23-24: Delete ⏎···

(prettier/prettier)

🤖 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 `@src/constants/email/email.constants.ts` around lines 23 - 24, The file has
formatting issues for the exported constant rescheduleMessage (and another at
the same file around line 56); run Prettier or reformat the string literals so
they are wrapped consistently and ensure the file ends with a single trailing
newline, then re-run lint/format checks; specifically update the
rescheduleMessage value in email.constants.ts to match project Prettier rules
(line-wrapping/quotes) and add the missing final newline.

* type: string
* example: Email was not sent
*/
router.post('/', sendEmail);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Protect this endpoint before exposing it publicly.

src/app.ts mounts emailRoutes at /api/email after only express.json() and logger, and this route accepts arbitrary recipient addresses. Right now anyone who can reach the API can use your Gmail account as a mail relay. Add the same auth/authorization middleware used for staff-only actions here, and update the Swagger docs with 401/403 once it is in place.

🤖 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 `@src/routes/email.routes.ts` at line 97, The POST /email route
(router.post('/', sendEmail)) is unprotected; wrap this route with the same
authentication and staff-authorization middleware used for staff-only actions
(apply the existing authenticate and authorizeStaff middleware functions in the
same order as other protected routes) so only authorized staff can send mail,
and ensure the route handler still receives the request after middleware. Also
update the Swagger/OpenAPI docs for this endpoint to include 401 and 403
responses and any required security schemes so clients know
authentication/authorization is required.

Comment on lines +23 to +29
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
Copy link
Copy Markdown

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

🌐 Web query:

What timeout options does Nodemailer createTransport support for SMTP transports, and are connectionTimeout, greetingTimeout, and socketTimeout valid in the currently used Nodemailer major version?

💡 Result:

Nodemailer's SMTP transport supports connectionTimeout, greetingTimeout, and socketTimeout as valid configuration options [1]. These options are part of the core SMTP transport configuration used in the current version of Nodemailer [1][2]. The following timeout options are supported: - connectionTimeout: Specifies the maximum time in milliseconds to wait for the TCP connection to be established. The default value is 120,000 ms (2 minutes) [3][1]. - greetingTimeout: Specifies the maximum time in milliseconds to wait for the server's initial greeting after the connection is established. The default value is 30,000 ms (30 seconds) [3][1]. - socketTimeout: Specifies the maximum time in milliseconds of inactivity allowed before the connection is automatically closed. The default value is 600,000 ms (10 minutes) [3][1]. Additionally, Nodemailer supports dnsTimeout (maximum time to wait for DNS resolution, defaulting to 30,000 ms) and dnsTtl (time to cache DNS lookup results, defaulting to 300,000 ms) [3][1]. These options are standard and have been consistently supported across major versions of Nodemailer, including earlier iterations [4][5].

Citations:


Add SMTP timeouts to the Nodemailer transporter (avoid request hang).

sendMail() runs on the request path and the current createTransport() config sets no SMTP timeouts. Nodemailer’s SMTP transport supports connectionTimeout, greetingTimeout, and socketTimeout, so add them to fail fast when Gmail is slow/unreachable.

Suggested change
 const transporter = nodemailer.createTransport({
   service: 'gmail',
+  connectionTimeout: 10_000,
+  greetingTimeout: 10_000,
+  socketTimeout: 15_000,
   auth: {
     user: process.env.SMTP_USER,
     pass: process.env.SMTP_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({
service: 'gmail',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const transporter = nodemailer.createTransport({
service: 'gmail',
connectionTimeout: 10_000,
greetingTimeout: 10_000,
socketTimeout: 15_000,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_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 `@src/services/email/email.service.ts` around lines 23 - 29, The Nodemailer
transporter created in createTransport lacks SMTP timeouts causing sendMail
(which runs on the request path) to potentially hang; update the transporter
creation (the const transporter / createTransport call in email.service.ts) to
include connectionTimeout, greetingTimeout, and socketTimeout (e.g., read from
env vars like SMTP_CONN_TIMEOUT, SMTP_GREETING_TIMEOUT, SMTP_SOCKET_TIMEOUT or
use sensible millisecond defaults) so the SMTP client fails fast when Gmail is
slow/unreachable.

Copy link
Copy Markdown
Collaborator

@GilHeller GilHeller left a comment

Choose a reason for hiding this comment

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

Great job. Please see my comments

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

add logs. when you catch and throw an error we might swallow the original error and that will cause the debugging to be significantly harder. (add a console.error(..,) before throwing an error)

Comment on lines +6 to +17
const attendanceEmailSchema = z.object({
emailType: z.literal(EMAIL_TYPE.ATTENDANCE_UPDATE),
email: z.string().trim().email(),
payload: z.object({
patientName: z.string().trim().min(1),
patientNumber: z.string().trim().min(1),
attendanceStatus: z.enum([ATTENDANCE_STATUS.COMING, ATTENDANCE_STATUS.NOT_COMING]),
time: z.string().trim().min(1),
cell: z.string().trim().min(1),
building: z.string().trim().min(1),
}),
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider moving this email schema definition to src/schemas/email/attendance-email.schema.ts or similar.


const sendEmailSchema = z.discriminatedUnion('emailType', [attendanceEmailSchema]);

type SendEmailData = z.infer<typeof sendEmailSchema>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider moving this type definition to the same file as attendanceEmailSchema (like src/schemas/email/attendance-email.schema.ts)

Comment on lines +31 to +39
const buildEmailContent = (data: SendEmailData): EmailContent => {
switch (data.emailType) {
case EMAIL_TYPE.ATTENDANCE_UPDATE:
return buildAttendanceEmailContent(data.payload);

default:
throw new Error(EMAIL_ERRORS.unsupportedEmailType);
}
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

consider moving if into src/schemas/email/email.service.helpers.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants