Skip to content

implement email service with zod and nodemailer#13

Open
Elad-Abutbul wants to merge 2 commits into
mainfrom
86extdhtj-emailService
Open

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

Conversation

@Elad-Abutbul

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

Copy link
Copy Markdown
Collaborator

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

  • New Features

    • Email notification system with attendance update messages
    • HTML email templates with status-specific styling and optional reschedule text
    • Built-in validation for incoming email data and clear success/error responses
  • Chores

    • Added runtime email delivery support and development typings for the email library
    • Added data validation library dependency

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request adds an attendance email feature: new dependencies (nodemailer, zod, types), Hebrew email constants, Zod validation schemas, email content builders (text + RTL HTML), a Gmail Nodemailer-backed sendEmail service, an Express POST /api/email route with Swagger docs, and app registration under /api/email.

Changes

Email Notification Feature

Layer / File(s) Summary
Email Constants & Configuration
package.json, src/constants/email/email.constants.ts
Dependencies added: nodemailer, zod, and @types/nodemailer. Email constants exported: EMAIL_TYPE, ATTENDANCE_STATUS, Hebrew EMAIL_SUBJECTS and EMAIL_TEXT templates, EMAIL_ERRORS/EMAIL_SUCCESS_MESSAGES, and EMAIL_COLORS.
Email Content Builders
src/services/email/email.service.helpers.ts
AttendanceEmailPayload and EmailContent types; escapeHtml and helpers that build plain-text message, table row HTML, and full RTL HTML email with status-specific header styling.
Validation Schemas & Service Core
src/schemas/email/attendance-email.schema.ts, src/schemas/email/email.service.helpers.ts, src/services/email/email.service.ts
attendanceEmailSchema and SendEmailData type; buildEmailContent dispatcher routing by emailType; sendEmailSchema discriminated union; Gmail Nodemailer transporter; sendEmail validates input, checks SMTP env vars, composes content, and dispatches email with error handling.
Email Controller, Routes & App Integration
src/controllers/email/email.controller.ts, src/routes/email.routes.ts, src/app.ts
sendEmail controller delegates to service and maps errors to 400/500; router defines POST /api/email with Swagger JSDoc and wires sendEmail; app imports and registers email routes at /api/email.

Sequence Diagram

sequenceDiagram
  participant Client
  participant EmailController
  participant EmailService
  participant Nodemailer
  participant Gmail
  Client->>EmailController: POST /api/email { emailType, payload }
  EmailController->>EmailService: sendEmail(data)
  EmailService->>EmailService: Validate with Zod (sendEmailSchema)
  EmailService->>EmailService: Check SMTP env vars (SMTP_USER/SMTP_PASS)
  EmailService->>EmailService: buildEmailContent(data)
  EmailService->>Nodemailer: transporter.sendMail({ from, to, subject, text, html })
  Nodemailer->>Gmail: SMTP send
  Gmail-->>Nodemailer: SMTP response
  Nodemailer-->>EmailService: send result Promise
  EmailService-->>EmailController: resolve/reject
  EmailController-->>Client: 200/400/500 with message
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through code with Zod in tow,
Nodemailer wrapped the sender’s bow.
RTL Hebrew, subjects clear and bright,
Attendance notes take flight tonight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: implementing an email service using Zod for validation and Nodemailer for sending, which aligns with the changeset's core functionality.
Description check ✅ Passed The description includes a summary of changes, implementation details, and completed checklist items. However, the 'Related Issue(s)' section lacks an actual issue number (shows placeholder format).
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.

@coderabbitai coderabbitai Bot 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.

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.

@GilHeller GilHeller left a comment

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.

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 thread src/services/email/email.service.ts Outdated
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.

Comment thread src/services/email/email.service.ts Outdated

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 thread src/services/email/email.service.ts Outdated
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

@Elad-Abutbul Elad-Abutbul requested a review from GilHeller June 7, 2026 10:51

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 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/schemas/email/attendance-email.schema.ts`:
- Line 2: attendance-email.schema.ts currently imports sendEmailSchema solely to
reuse the SendEmailData type, creating unnecessary coupling with email.service;
instead derive SendEmailData from attendanceEmailSchema itself. Remove the
import of sendEmailSchema, export or declare a type like SendEmailData =
z.infer<typeof attendanceEmailSchema> (reference attendanceEmailSchema and
SendEmailData) so email.service.ts can import the type from
attendance-email.schema.ts without creating a schema↔service import cycle;
update any usages that referenced the old sendEmailSchema type to use the new
derived SendEmailData.
🪄 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: 8637b5a3-5d19-476b-8a82-fda58dc1c090

📥 Commits

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

📒 Files selected for processing (3)
  • src/schemas/email/attendance-email.schema.ts
  • src/schemas/email/email.service.helpers.ts
  • src/services/email/email.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/services/email/email.service.ts

@@ -0,0 +1,18 @@
import { ATTENDANCE_STATUS, EMAIL_TYPE } from '@/constants/email/email.constants';
import { sendEmailSchema } from '@/services/email/email.service';

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether TS config preserves non-type imports and confirm dependency cycle edges.
fd -i 'tsconfig*.json' .
rg -n --no-heading -e 'verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues' $(fd -i 'tsconfig*.json' .)

# Confirm cross-imports between schema and service files.
rg -n --no-heading -e "from '`@/services/email/email.service`'" -e "from '`@/schemas/email/attendance-email.schema`'" src/services/email/email.service.ts src/schemas/email/attendance-email.schema.ts

Repository: ColmanDevClubORG/Sagol360ManagementServer

Length of output: 333


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== tsconfig.json (relevant compilerOptions) ==="
cat -n tsconfig.json | sed -n '1,220p' | rg -n "compilerOptions|verbatimModuleSyntax|preserveValueImports|importsNotUsedAsValues|module|target|moduleResolution|esModuleInterop|isolatedModules|useDefineForClassFields" || true

echo
echo "=== attendance-email.schema.ts (imports + SendEmailData) ==="
nl -ba src/schemas/email/attendance-email.schema.ts | sed -n '1,120p'

echo
echo "=== email.service.ts (imports + attendanceEmailSchema usage) ==="
nl -ba src/services/email/email.service.ts | sed -n '1,160p'

Repository: ColmanDevClubORG/Sagol360ManagementServer

Length of output: 440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== tsconfig.json (compilerOptions excerpt) ==="
# Print the compilerOptions block only (small)
python3 - <<'PY'
import json
p='tsconfig.json'
with open(p,'r',encoding='utf-8') as f: data=json.load(f)
co=data.get('compilerOptions',{})
keys=[
'verbatimModuleSyntax',
'preserveValueImports',
'importHelpers',
'importsNotUsedAsValues',
'isolatedModules',
'module',
'moduleResolution',
'target',
'esModuleInterop',
'useUnknownInCatchVariables',
'strict',
'requireEmitForEachFunction',
'importsNotUsedAsValues'
]
print(json.dumps({k:co.get(k) for k in keys if k in co or k in ['module','moduleResolution','target','esModuleInterop']},indent=2))
PY

echo
echo "=== attendance-email.schema.ts (first 80 lines) ==="
cat -n src/schemas/email/attendance-email.schema.ts | sed -n '1,120p'

echo
echo "=== email.service.ts (first 140 lines) ==="
cat -n src/services/email/email.service.ts | sed -n '1,160p'

Repository: ColmanDevClubORG/Sagol360ManagementServer

Length of output: 3089


Remove the schema↔service import coupling by deriving SendEmailData from attendanceEmailSchema

attendance-email.schema.ts imports sendEmailSchema only for SendEmailData typing, while email.service.ts uses sendEmailSchema at runtime and imports attendanceEmailSchema back—creating a circular dependency. With the current tsconfig (NodeNext; no verbatimModuleSyntax/preserveValueImports/importsNotUsedAsValues overrides shown), this is likely not a runtime-cycle issue, but it’s still unnecessary coupling; derive the type directly from attendanceEmailSchema.

Suggested change
 import { ATTENDANCE_STATUS, EMAIL_TYPE } from '`@/constants/email/email.constants`';
-import { sendEmailSchema } from '`@/services/email/email.service`';
 import { z } from 'zod';
@@
-export type SendEmailData = z.infer<typeof sendEmailSchema>;
+export type SendEmailData = z.infer<typeof attendanceEmailSchema>;
🤖 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/schemas/email/attendance-email.schema.ts` at line 2,
attendance-email.schema.ts currently imports sendEmailSchema solely to reuse the
SendEmailData type, creating unnecessary coupling with email.service; instead
derive SendEmailData from attendanceEmailSchema itself. Remove the import of
sendEmailSchema, export or declare a type like SendEmailData = z.infer<typeof
attendanceEmailSchema> (reference attendanceEmailSchema and SendEmailData) so
email.service.ts can import the type from attendance-email.schema.ts without
creating a schema↔service import cycle; update any usages that referenced the
old sendEmailSchema type to use the new derived SendEmailData.

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