Skip to content

Conversation

@sameh0
Copy link

@sameh0 sameh0 commented Nov 17, 2025

Describe your changes

Fixes Docker template loading and stops TLS flags from being passed when TLS isn't needed.
What Changed

  • TLS is now conditional: Only adds TLS settings when systemEmailSecure is enabled. Previously, TLS flags were always passed, breaking services like Resend that don't require TLS config.
  • Better validation: Catches missing fields, invalid emails, and empty content before sending
  • Template loading: Tries 6 different paths so templates work in dev and Docker
    Improved errors: Clearer logs instead of cryptic nodemailer failures

Fixes:

  • ENOENT errors for templates in Docker
  • "Missing html or text field" errors from empty content
  • Can now use SMTP services that require no TLS configuration (like resend)

Write your issue number after "Fixes "

Fixes #3054

Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks.

  • (Do not skip this or your PR will be closed) I deployed the application locally.
  • (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code.
  • I have included the issue # in the PR.
  • I have added i18n support to visible strings (instead of <div>Add</div>, use):
const { t } = useTranslation();
<div>{t('add')}</div>
  • I have not included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
  • I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
  • I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions.
  • My PR is granular and targeted to one specific feature.
  • I ran npm run format in server and client directories, which automatically formats your code.
  • I took a screenshot or a video and attached to this PR if there is a UI change.

Summary by CodeRabbit

  • Bug Fixes

    • Stronger validation and error handling for template loading, email construction, and sending to reduce runtime failures and return safe fallbacks
    • Templates are discovered across multiple locations with safe no-op fallbacks when missing
    • Added transporter verification, clearer send errors, fallback HTML, and safer handling of sender address and TLS application
  • UI

    • Settings screen shows TLS fields only when applicable, producing a simplified configuration JSON when TLS is not configured

# Conflicts:
#	server/src/service/v1/infrastructure/emailService.js
@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

Warning

Rate limit exceeded

@sameh0 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 5 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between fe27ce2 and 515d6cf.

📒 Files selected for processing (1)
  • client/src/Pages/v1/Settings/SettingsEmail.jsx (1 hunks)

Note

.coderabbit.yml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'release_notes'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Server email service: adds multi-path template discovery with a no-op fallback, stronger template and input validation, conditional TLS application, transporter verification before sending, and explicit false returns on failures. Client settings UI now conditionally includes TLS fields in the displayed email configuration JSON.

Changes

Cohort / File(s) Summary
Email service core
server/src/service/v1/infrastructure/emailService.js
Multi-path template discovery (dev → prod) with compile-first-found and no-op fallback; enhanced error logging and template validation; validate MJML → HTML non-empty; sendEmail input validation (to, subject), compute/validate fromAddress, fallback HTML; transporter.verify() before send; TLS options moved to top-level (ignoreTLS, requireTLS) and applied only when systemEmailSecure true; preloads templates into templateLookup.
Client settings UI
client/src/Pages/v1/Settings/SettingsEmail.jsx
JSON display now conditionally includes TLS-related fields: only emits tls and its rejectUnauthorized/servername when systemEmailSecure is true and values exist; ignoreTLS/requireTLS included only when defined in secure mode.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant EmailSvc as emailService
    participant Loader as TemplateLoader
    participant Builder as EmailBuilder
    participant MJML as MJMLCompiler
    participant SMTP as SMTPTransporter

    Caller->>EmailSvc: sendEmail(to, subject, templateName, data)

    rect rgb(240,248,255)
    Note over EmailSvc: Input validation & fromAddress
    EmailSvc->>EmailSvc: validate to/subject, compute fromAddress
    alt invalid input
        EmailSvc-->>Caller: return false
    end
    end

    rect rgb(255,250,240)
    Note over Loader: Template discovery (dev → prod)
    EmailSvc->>Loader: loadTemplate(templateName)
    Loader->>Loader: probe dev paths → probe prod/dist paths
    alt found
        Loader-->>EmailSvc: compiled template
    else not found
        Loader-->>EmailSvc: no-op template
    end
    end

    rect rgb(240,255,240)
    Note over Builder: Build & validate content
    EmailSvc->>Builder: buildEmail(template, data)
    Builder->>MJML: compile MJML -> HTML
    alt empty/invalid
        Builder-->>EmailSvc: throw / return error
    else valid HTML
        Builder-->>EmailSvc: HTML ready
    end
    end

    rect rgb(255,245,240)
    Note over SMTP: Verify & send
    EmailSvc->>SMTP: transporter.verify()
    alt verify fails
        SMTP-->>EmailSvc: error
        EmailSvc-->>Caller: return false
    else verify OK
        EmailSvc->>SMTP: sendMail({from: fromAddress, ...})
        alt send succeeds
            SMTP-->>EmailSvc: success
            EmailSvc-->>Caller: return true
        else send fails
            SMTP-->>EmailSvc: error
            EmailSvc-->>Caller: return false
        end
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Focus areas:
    • multi-path template discovery correctness and no-op fallback
    • MJML -> HTML validation and error handling
    • conditional TLS application when systemEmailSecure is true
    • transporter.verify() handling and consistent false returns
    • client UI conditional inclusion of TLS fields

Poem

🐇
I hopped through folders, found each file,
Compiled the mail with careful smile.
Verified the post, checked every TLS seam,
Now alerts arrive and emails gleam. ✉️🎉

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: email template loading fixes and conditional TLS configuration, matching the primary objectives.
Description check ✅ Passed Description comprehensively addresses all major changes with clear problem statements, solutions, and fixes; all required checklist items are marked complete.
Linked Issues check ✅ Passed Code changes directly address issue #3054: template loading across multiple paths fixes ENOENT errors, validation prevents empty emails, and conditional TLS enables services like Resend.
Out of Scope Changes check ✅ Passed All changes focus on email service improvements and UI display of email configuration; no unrelated modifications detected across the two modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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.

@sameh0
Copy link
Author

sameh0 commented Nov 17, 2025

Image exist for testing at ghcr.io/sameh0/checkmate-backend:latest

Copy link

@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: 2

🧹 Nitpick comments (1)
server/src/service/v1/infrastructure/emailService.js (1)

49-62: Template multi-path loading and error handling look solid, minor logging nit only

The multi-path search with early exit and structured debug/error logging is a clear improvement and should resolve the Docker vs. dev template path issues as intended. The no-op fallback function combined with the later empty-MJML guard in buildEmail is a reasonable way to avoid hard startup failures while still surfacing problems when a template is actually used.

One small optional improvement: the ENOENT error message currently logs the full list of absolute possiblePaths (including process.cwd()-based ones), which may expose internal filesystem layout. You could consider logging them as paths relative to process.cwd() or __dirname instead, using something like this.path.relative(process.cwd(), p) rather than .replace(__dirname, ".").

Also applies to: 64-66, 68-82, 84-99, 101-105, 108-109

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57ceac0 and ee0ad25.

📒 Files selected for processing (1)
  • server/src/service/v1/infrastructure/emailService.js (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
server/src/service/v1/infrastructure/emailService.js (1)
server/src/index.js (2)
  • __dirname (19-19)
  • SERVICE_NAME (13-13)
🔇 Additional comments (2)
server/src/service/v1/infrastructure/emailService.js (2)

137-145: Stronger template and MJML/HTML validation aligns with the PR goals

The additional checks in buildEmail (template existence, function type, non-empty MJML, and non-empty HTML) plus the structured error logging and rethrow are all good improvements. They should prevent silent empty emails and make template/config issues much easier to track down, while keeping behavior explicit (exceptions instead of returning bad content).

No issues spotted with this block.

Also applies to: 146-154, 158-167, 171-182, 187-191, 194-195


310-311: Using validated fromAddress and returning false on send errors looks good

Switching the from field to use the validated fromAddress and normalizing send failures to return false gives a clearer and more predictable contract to callers. This is consistent with the earlier input validation and should make downstream handling simpler.

No issues here.

Also applies to: 322-323

@sameh0 sameh0 changed the title Fixing email validation Fix email template loading and conditional TLS configuration Nov 17, 2025
Copy link

@llamapreview llamapreview bot left a comment

Choose a reason for hiding this comment

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

AI Code Review by LlamaPReview

🎯 TL;DR & Recommendation

Recommendation: Request Changes

This PR improves email validation and template loading but introduces breaking changes in TLS configuration and from address validation, along with silent template failures that could lead to empty emails.

📄 Documentation Diagram

This diagram documents the refactored email sending workflow with improved validation and conditional TLS.

sequenceDiagram
    participant U as User
    participant ES as EmailService
    participant SS as SMTP Server
    U->>ES: sendEmail(to, subject, html)
    ES->>ES: Validate parameters
    note over ES: PR #35;3066 added validation<br/>for empty content and email format
    ES->>ES: Build email config
    note over ES: Conditional TLS settings<br/>only if systemEmailSecure true
    ES->>SS: Send email with config
    SS-->>ES: Response
Loading

🌟 Strengths

  • Enhanced template loading with multiple fallback paths for better reliability in different environments.
  • Improved error handling and logging throughout the email service.
Priority File Category Impact Summary Anchors
P1 server/.../emailService.js Architecture Breaking change in TLS config could cause connection failures. settingsController.js
P1 server/.../emailService.js Bug From address validation may incorrectly fail valid SMTP setups. settingsController.js
P1 server/.../emailService.js Bug Silent template failures lead to empty emails without clear errors. notificationService.js, settingsController.js
P2 server/.../emailService.js Bug Empty HTML validation breaks notification flows with degraded functionality.
P2 server/.../emailService.js Maintainability Complex template path resolution increases code maintenance burden.
P2 server/.../emailService.js Maintainability Template validation exposes implementation details in logs.

🔍 Notable Themes

  • Configuration Breaking Changes: TLS and from address validations may disrupt existing SMTP setups without clear migration paths.
  • Error Handling Consistency: Mixed approaches to failures—some propagated, some silenced—could confuse debugging efforts.

📈 Risk Diagram

This diagram illustrates the risks in TLS configuration changes and validation logic.

sequenceDiagram
    participant SC as Settings Controller
    participant ES as EmailService
    participant NS as Notification Service
    SC->>ES: Provide email config<br/>(includes TLS settings)
    note over SC,ES: R1(P1): TLS settings ignored<br/>if systemEmailSecure false
    ES->>ES: Validate from address
    note over ES: R2(P1): Incorrect validation<br/>may fail valid setups
    ES->>ES: Load template
    note over ES: R3(P1): Silent failure<br/>returns empty function
    ES->>NS: Send email (may fail or send empty)
Loading

💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.

if (systemEmailRequireTLS !== undefined) {
tlsSettings.requireTLS = systemEmailRequireTLS;
}
if (systemEmailTLSServername !== undefined && systemEmailTLSServername !== null && systemEmailTLSServername !== '') {
Copy link

Choose a reason for hiding this comment

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

P1 | Confidence: High

This change conditionally applies TLS settings only when systemEmailSecure is enabled. However, the related context shows that settingsController.js passes all TLS-related settings (including systemEmailRejectUnauthorized, systemEmailIgnoreTLS, etc.) regardless of the secure flag. This creates a potential breaking change where users with systemEmailSecure: false but explicit TLS settings will lose their TLS configuration, potentially causing connection failures or security issues with SMTP servers that require specific TLS behavior even in non-secure mode.

Code Suggestion:

// Apply TLS settings if any TLS-related config is present
const tlsSettings = {};
if (systemEmailRejectUnauthorized !== undefined) {
    tlsSettings.rejectUnauthorized = systemEmailRejectUnauthorized;
}
if (systemEmailIgnoreTLS !== undefined) {
    tlsSettings.ignoreTLS = systemEmailIgnoreTLS;
}
if (systemEmailRequireTLS !== undefined) {
    tlsSettings.requireTLS = systemEmailRequireTLS;
}
if (systemEmailTLSServername !== undefined && systemEmailTLSServername !== null && systemEmailTLSServername !== '') {
    tlsSettings.servername = systemEmailTLSServername;
}
if (Object.keys(tlsSettings).length > 0) {
    emailConfig.tls = tlsSettings;
}

Evidence: path:server/src/controllers/v1/settingsController.js

Comment on lines 241 to 250
// Validate from address
const fromAddress = systemEmailAddress || systemEmailUser;
if (!fromAddress || !fromAddress.includes('@')) {
this.logger.error({
message: `Invalid from email address: ${fromAddress}`,
service: SERVICE_NAME,
method: "sendEmail",
});
return false;
}
Copy link

Choose a reason for hiding this comment

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

P1 | Confidence: High

The new validation rejects emails if the from address doesn't contain '@'. However, the related context shows that settingsController.js passes systemEmailUser separately from systemEmailAddress. Some SMTP configurations may use systemEmailUser as a username (not necessarily an email) while systemEmailAddress is the actual from address. This validation could incorrectly fail valid configurations where systemEmailUser is a non-email username and systemEmailAddress is properly set.

Code Suggestion:

const fromAddress = systemEmailAddress;
if (!fromAddress || !fromAddress.includes('@')) {
    this.logger.error({
        message: `Invalid from email address: ${fromAddress}`,
        service: SERVICE_NAME,
        method: "sendEmail",
    });
    return false;
}

Evidence: path:server/src/controllers/v1/settingsController.js

Comment on lines +108 to +109
// Return a no-op function that returns empty string to prevent runtime errors
return () => "";
Copy link

Choose a reason for hiding this comment

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

P1 | Confidence: High

When template loading fails, the code returns a function that produces empty content. This creates a silent failure mode where emails are sent with empty content instead of proper error handling. The related context shows that both notificationService.js and settingsController.js rely on sendEmail returning a messageId for success, but they won't receive clear errors about template failures, making debugging difficult.

Code Suggestion:

// Re-throw the error to let callers handle template failures properly
throw new Error(`Failed to load template '${templateName}': ${error.message}`);

Evidence: path:server/src/service/v1/infrastructure/notificationService.js, path:server/src/controllers/v1/settingsController.js

return this.compile(templateContent);
// Try multiple possible paths for template files
// to support both development and production environments
const possiblePaths = [
Copy link

Choose a reason for hiding this comment

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

P2 | Confidence: High

The template path resolution now tries 6 different locations, which increases complexity and makes the code harder to maintain. The paths mix development, production, and build artifacts locations, creating potential confusion about which path should be used in different environments. This approach may mask configuration issues rather than solving them properly.

Code Suggestion:

const templateBase = process.env.EMAIL_TEMPLATE_PATH || this.path.join(process.cwd(), 'templates');
const templatePath = this.path.join(templateBase, `${templateName}.mjml`);

return false;
}

// Validate HTML content
Copy link

Choose a reason for hiding this comment

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

P2 | Confidence: Medium

The new validation rejects empty HTML content, but the related context shows notificationService.js calls sendEmail without checking the HTML content first. This could break notification flows where template rendering fails silently. While preventing empty emails is good, the change introduces a hard failure where previously notifications might have continued with degraded functionality.

Code Suggestion:

if (!html || html.trim() === "") {
    this.logger.warn({
        message: "Email HTML content is empty, using fallback text",
        service: SERVICE_NAME,
        method: "sendEmail",
    });
    html = "<p>Email content unavailable</p>";
}

Comment on lines 146 to 154
if (typeof this.templateLookup[template] !== 'function') {
this.logger.error({
message: `Template '${template}' is not a function. Type: ${typeof this.templateLookup[template]}`,
service: SERVICE_NAME,
method: "buildEmail",
templateValue: this.templateLookup[template],
});
throw new Error(`Template '${template}' is not a function`);
}
Copy link

Choose a reason for hiding this comment

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

P2 | Confidence: Medium

This validation exposes internal implementation details (template functions) in error messages. The templateValue logging could leak sensitive template content or create overly verbose logs. The check assumes all templates must be functions, which couples the validation to the current implementation rather than the interface contract.

Code Suggestion:

if (typeof this.templateLookup[template] !== 'function') {
    this.logger.error({
        message: `Template '${template}' is not properly initialized`,
        service: SERVICE_NAME,
        method: "buildEmail",
    });
    throw new Error(`Template '${template}' is not available`);
}

Copy link

@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: 1

♻️ Duplicate comments (4)
server/src/service/v1/infrastructure/emailService.js (4)

146-154: Remove or redact templateValue from error logs.

Logging templateValue (line 151) could leak sensitive template content or create overly verbose logs. The type check and available templates list provide sufficient debugging context.

 			if (typeof this.templateLookup[template] !== 'function') {
 				this.logger.error({
 					message: `Template '${template}' is not a function. Type: ${typeof this.templateLookup[template]}`,
 					service: SERVICE_NAME,
 					method: "buildEmail",
-					templateValue: this.templateLookup[template],
 				});
 				throw new Error(`Template '${template}' is not a function`);
 			}

51-62: Simplify template path resolution with environment variable.

Trying 6 hardcoded paths adds complexity and makes the code harder to maintain. The paths mix development, production, and build artifact locations, which can mask configuration issues rather than solve them properly.

Consider using a single environment variable to specify the template directory, with a sensible default:

-				const possiblePaths = [
-					// Production/Docker path - templates in dist/templates (from dist/src/service/v1/infrastructure)
-					this.path.join(__dirname, `../../../templates/${templateName}.mjml`),
-					// Alternative production path - templates in dist/templates (from dist/service/v1/infrastructure)
-					this.path.join(__dirname, `../../templates/${templateName}.mjml`),
-					// If running from dist, templates might be in parent src directory
-					this.path.join(__dirname, `../../../../src/templates/${templateName}.mjml`),
-					// Development path - from the project root
-					this.path.join(process.cwd(), `templates/${templateName}.mjml`),
-					this.path.join(process.cwd(), `src/templates/${templateName}.mjml`),
-					this.path.join(process.cwd(), `dist/templates/${templateName}.mjml`),
-				];
-
-				let templatePath;
-				let templateContent;
-
-				// Try each path until we find one that works
-				for (const tryPath of possiblePaths) {
-					try {
-						if (this.fs.existsSync(tryPath)) {
-							templatePath = tryPath;
-							templateContent = this.fs.readFileSync(templatePath, "utf8");
-							break;
-						}
-					} catch (e) {
-						// Continue to next path
-					}
-				}
-
-				if (!templateContent) {
-					throw new Error(`Template file not found in any of: ${possiblePaths.map(p => p.replace(__dirname, '.')).join(', ')}`);
-				}
+				const templateBase = process.env.EMAIL_TEMPLATE_PATH || this.path.join(process.cwd(), 'templates');
+				const templatePath = this.path.join(templateBase, `${templateName}.mjml`);
+				
+				if (!this.fs.existsSync(templatePath)) {
+					throw new Error(`Template file not found at: ${templatePath}`);
+				}
+				
+				const templateContent = this.fs.readFileSync(templatePath, "utf8");

Then configure EMAIL_TEMPLATE_PATH in your Docker environment to point to the correct location.


108-109: Throw error on template load failure instead of returning empty function.

Returning a no-op function that produces empty content creates a silent failure mode. Emails will be sent with empty content instead of proper error handling, making debugging difficult.

The validation added in buildEmail (lines 158-167) will catch this and throw, but it's better to fail fast at template loading time:

-				// Return a no-op function that returns empty string to prevent runtime errors
-				return () => "";
+				// Re-throw the error to let callers handle template failures properly
+				throw error;

This ensures template loading issues are detected during initialization rather than at email-send time.


199-219: Remove PII from error logs and document HTML-only requirement.

Two concerns with the validation logic:

  1. PII exposure (lines 215-216): Logging to and subject directly exposes email addresses in logs, which may violate compliance/privacy requirements. Remove these fields or redact them:
 		if (!html || html.trim() === "") {
 			this.logger.error({
 				message: "Cannot send email: HTML content is empty",
 				service: SERVICE_NAME,
 				method: "sendEmail",
-				to: to,
-				subject: subject,
 			});
 			return false;
 		}
  1. HTML-only enforcement (line 210): The validation blocks any text-only emails. If this is intentional, document it; if text-only support should remain, accept either html or text parameters. Currently no callers use text-only, but this silently prevents future usage.
🧹 Nitpick comments (1)
server/src/service/v1/infrastructure/emailService.js (1)

241-250: From address validation looks correct, but consider clearer error messaging.

The logic correctly prioritizes systemEmailAddress over systemEmailUser, so the concern from past reviews about username vs email is addressed. The validation will only fail if neither value contains an '@' symbol.

However, the error message could be more helpful by indicating which configuration field needs to be fixed:

 		const fromAddress = systemEmailAddress || systemEmailUser;
 		if (!fromAddress || !fromAddress.includes('@')) {
 			this.logger.error({
-				message: "Invalid from email address",
+				message: "Invalid from email address: systemEmailAddress or systemEmailUser must be a valid email",
 				service: SERVICE_NAME,
 				method: "sendEmail",
 			});
 			return false;
 		}

Good practice: The error log doesn't expose the actual email address, avoiding PII leakage.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee0ad25 and 0b3e023.

📒 Files selected for processing (1)
  • server/src/service/v1/infrastructure/emailService.js (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
server/src/service/v1/infrastructure/emailService.js (1)
server/src/index.js (1)
  • __dirname (19-19)
🔇 Additional comments (1)
server/src/service/v1/infrastructure/emailService.js (1)

296-326: Email sending logic is well-structured.

The transporter verification and email sending flow is correctly implemented:

  • Transporter is verified before use (lines 298-307)
  • Uses the validated fromAddress (line 312)
  • Consistent error handling that returns false on failure (lines 306, 324)
  • Returns messageId on success (line 316)

This provides clear success/failure signaling to callers.

Copy link

@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: 0

♻️ Duplicate comments (3)
server/src/service/v1/infrastructure/emailService.js (3)

265-295: TLS configuration inconsistency: ignoreTLS and requireTLS should also be conditional.

Lines 266-271 unconditionally apply ignoreTLS and requireTLS, while lines 274-295 only apply rejectUnauthorized and servername when systemEmailSecure is true. This is inconsistent with the PR objective: "only add TLS settings when systemEmailSecure is enabled; previously TLS flags were always passed, breaking services (e.g., Resend)."

A previous review comment on these exact lines provided the fix. All TLS-related options should only be added when systemEmailSecure is true.

Apply this diff to make all TLS settings conditional:

 	// Build base email config
 	const emailConfig = {
 		host: systemEmailHost,
 		port: Number(systemEmailPort),
 		secure: systemEmailSecure,
 		auth: {
 			user: systemEmailUser || systemEmailAddress,
 			pass: systemEmailPassword,
 		},
 		name: systemEmailConnectionHost || "localhost",
 		connectionTimeout: 5000,
 	};
 
-	// Add top-level TLS options (must be outside tls object)
-	if (systemEmailIgnoreTLS !== undefined) {
-		emailConfig.ignoreTLS = systemEmailIgnoreTLS;
-	}
-	if (systemEmailRequireTLS !== undefined) {
-		emailConfig.requireTLS = systemEmailRequireTLS;
-	}
-
 	// Conditionally add TLS settings only if secure is enabled
 	if (systemEmailSecure) {
 		const tlsSettings = {};
 
+		// Add top-level TLS options (must be outside tls object)
+		if (systemEmailIgnoreTLS !== undefined) {
+			emailConfig.ignoreTLS = systemEmailIgnoreTLS;
+		}
+		if (systemEmailRequireTLS !== undefined) {
+			emailConfig.requireTLS = systemEmailRequireTLS;
+		}
+
 		// Only add TLS settings that are explicitly configured
 		if (systemEmailRejectUnauthorized !== undefined) {
 			tlsSettings.rejectUnauthorized = systemEmailRejectUnauthorized;
 		}
 		if (systemEmailTLSServername !== undefined && systemEmailTLSServername !== null && systemEmailTLSServername !== '') {
 			tlsSettings.servername = systemEmailTLSServername;
 		}
 
 		// Only add tls property if we have TLS settings
 		if (Object.keys(tlsSettings).length > 0) {
 			emailConfig.tls = tlsSettings;
 			this.logger.debug({
 				message: `TLS settings applied to email config`,
 				service: SERVICE_NAME,
 				method: "sendEmail",
 				tlsSettings: Object.keys(tlsSettings),
 			});
 		}
 	}

209-219: PII exposure: email addresses and subjects still logged directly.

Despite a previous review comment (Major, marked as "Addressed in commit 0b3e023") flagging PII exposure in error logs, lines 215-216 still log the recipient email address (to) and subject directly. Email addresses are PII and subjects may contain sensitive information.

Apply this diff to remove PII from logs:

 	// Validate HTML content
 	if (!html || html.trim() === "") {
 		this.logger.warn({
 			message: "Email HTML content is empty, using fallback text",
 			service: SERVICE_NAME,
 			method: "sendEmail",
-			to: to,
-			subject: subject,
 		});
 		html = "<p>Email content unavailable</p>";
 	}

108-109: Silent failure still masks template loading errors.

Returning an empty-string function when template loading fails creates a silent failure mode where emails are sent with empty content instead of surfacing the error to callers. This contradicts the PR's goal of improved error handling and makes debugging difficult.

A previous review comment (P1, High confidence) on these lines recommended re-throwing the error to let callers handle template failures properly. The current implementation still suppresses the error.

Apply this diff to surface template loading failures:

 		} catch (error) {
 			this.logger.error({
 				message: `Failed to load template '${templateName}': ${error.message}`,
 				service: SERVICE_NAME,
 				method: "loadTemplate",
 				templateName: templateName,
 				error: error.message,
 				stack: error.stack,
 			});
-			// Return a no-op function that returns empty string to prevent runtime errors
-			return () => "";
+			// Re-throw to let callers handle the failure
+			throw new Error(`Failed to load template '${templateName}': ${error.message}`);
 		}
🧹 Nitpick comments (1)
server/src/service/v1/infrastructure/emailService.js (1)

146-154: Remove templateValue from error logs to avoid exposing template internals.

A previous review comment (P2, Medium confidence) flagged that logging templateValue could leak sensitive template content or create overly verbose logs. The type and template name provide sufficient debugging context.

Apply this diff:

 		if (typeof this.templateLookup[template] !== 'function') {
 			this.logger.error({
 				message: `Template '${template}' is not a function. Type: ${typeof this.templateLookup[template]}`,
 				service: SERVICE_NAME,
 				method: "buildEmail",
-				templateValue: this.templateLookup[template],
 			});
 			throw new Error(`Template '${template}' is not a function`);
 		}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b3e023 and 621d3c7.

📒 Files selected for processing (1)
  • server/src/service/v1/infrastructure/emailService.js (6 hunks)

@gorkem-bwl
Copy link
Contributor

@sameh0 your PR has a minor CI/CD check issue:

Checking formatting...
[warn] src/service/v1/infrastructure/emailService.js
[warn] Code style issues found in the above file. Run Prettier with --write to fix.
Error: Process completed with exit code 1.

Copy link

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/src/service/v1/infrastructure/emailService.js (1)

198-250: Undefined redactEmail / redactSubject will throw on empty-HTML paths

In sendEmail, the warning branch for empty HTML (Lines 210–218) calls this.redactEmail(to) and this.redactSubject(subject), but neither helper is defined on EmailService. This will cause a TypeError: this.redactEmail is not a function the first time an email is built with empty HTML, preventing the fallback HTML from being used and short‑circuiting the send.

Given the privacy concerns around logging PII, the simplest safe fix is to drop these fields from the log for now:

		if (!html || html.trim() === "") {
			this.logger.warn({
				message: "Email HTML content is empty, using fallback text",
				service: SERVICE_NAME,
				method: "sendEmail",
-				to: this.redactEmail(to),
-				subject: this.redactSubject(subject),
			});
			html = "<p>Email content unavailable</p>";
		}

If you do want redacted to/subject in logs, please add concrete helper implementations on the class (or inject a masking utility) and keep them thoroughly unit‑tested.

🧹 Nitpick comments (3)
server/src/service/v1/infrastructure/emailService.js (3)

47-110: Template multi-path loading is robust; consider surfacing non‑ENOENT fs errors

The multi-path resolution and logging of all attempted locations should help a lot with dev vs Docker discrepancies, and the final error message is clear. However, the inner try { ... } catch (e) { /* ignore */ } around existsSync/readFileSync means real I/O problems (e.g., permission errors, partial reads) get silently swallowed and later misreported as “file not found”.

A small refactor would be to only suppress ENOENT and let other errors bubble to the outer catch, for example:

-		for (const tryPath of possiblePaths) {
-			try {
-				if (this.fs.existsSync(tryPath)) {
-					templatePath = tryPath;
-					templateContent = this.fs.readFileSync(templatePath, "utf8");
-					break;
-				}
-			} catch (e) {
-				// Continue to next path
-			}
-		}
+		for (const tryPath of possiblePaths) {
+			try {
+				if (this.fs.existsSync(tryPath)) {
+					templatePath = tryPath;
+					templateContent = this.fs.readFileSync(templatePath, "utf8");
+					break;
+				}
+			} catch (e) {
+				// Only ignore "file not found" errors; surface everything else
+				if (!e || e.code !== "ENOENT") {
+					throw e;
+				}
+			}
+		}

This preserves the fallback behavior while keeping error diagnostics accurate when something is genuinely wrong with the file system.


252-296: TLS option gating may render ignoreTLS / requireTLS ineffective—confirm this matches intent

The TLS block currently applies all TLS-related options only when systemEmailSecure is true:

if (systemEmailSecure) {
  if (systemEmailIgnoreTLS !== undefined) {
    emailConfig.ignoreTLS = systemEmailIgnoreTLS;
  }
  if (systemEmailRequireTLS !== undefined) {
    emailConfig.requireTLS = systemEmailRequireTLS;
  }
  // ...
}

Per nodemailer’s docs, ignoreTLS and requireTLS specifically control STARTTLS behavior and only have effect when secure is false (STARTTLS mode). With the current wiring:

  • When systemEmailSecure is false (typical for port 587 STARTTLS), these flags are never applied, so admin-configured ignoreTLS / requireTLS values become no-ops.
  • When systemEmailSecure is true (implicit TLS), the flags are set but effectively irrelevant.

If the product intentionally wants to ignore ignoreTLS / requireTLS whenever secure is false (to protect providers like Resend), consider documenting that in code comments and/or UI so operators don’t expect these flags to do anything in that configuration.

If you do want these options to behave as in nodemailer’s documentation while still protecting non‑TLS providers, you may want to adjust the conditions—for example, only applying them when admins explicitly enable them and accepting that this opt‑in might break misconfigured Resend setups, or splitting “TLS enabled” from “advanced TLS tuning” into separate config flags.


299-325: Consider logging error details on transporter verification failures

The verify step is a good addition, but the catch block only logs a generic "Email transporter verification failed" message and drops error.message / connection details. That can make SMTP misconfigurations (wrong host, port, TLS mismatch) hard to troubleshoot from logs alone.

A small, non‑breaking improvement:

		try {
			await this.transporter.verify();
		} catch (error) {
			this.logger.warn({
-				message: "Email transporter verification failed",
+				message: `Email transporter verification failed: ${error.message}`,
				service: SERVICE_NAME,
				method: "verifyTransporter",
+				host: emailConfig.host,
+				port: emailConfig.port,
+				secure: emailConfig.secure,
+				stack: error.stack,
			});
			return false;
		}

This keeps verification as a soft failure (false return) but makes diagnosing configuration issues much easier.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 621d3c7 and ed5758f.

📒 Files selected for processing (1)
  • server/src/service/v1/infrastructure/emailService.js (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
server/src/service/v1/infrastructure/emailService.js (1)
server/src/index.js (1)
  • __dirname (19-19)
🔇 Additional comments (1)
server/src/service/v1/infrastructure/emailService.js (1)

135-195: Stronger template / MJML validation looks good

The additional guards in buildEmail (missing template entry, non-function, empty MJML, and missing HTML from mjml2html) plus rethrowing after logging eliminate the previous “silent empty email” failure mode and propagate clear errors to callers. This aligns well with the goal of avoiding empty infrastructure alert emails.

No issues from my side here.

Copy link

@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: 1

🧹 Nitpick comments (1)
client/src/Pages/v1/Settings/SettingsEmail.jsx (1)

294-307: Inner TLS field conditions are also affected by default values.

While the outer condition correctly checks if systemEmailSecure is truthy, the inner conditions at lines 299-301 also check !== undefined, which will always be true for systemEmailRejectUnauthorized (defaults to true at line 45).

This means when systemEmailSecure is true, the tls object will always include rejectUnauthorized, even if you intended to make it conditional.

Consider the same fix approaches as the previous comment:

 	...(systemEmailSecure &&
-		(systemEmailRejectUnauthorized !== undefined ||
-			(systemEmailTLSServername &&
-				systemEmailTLSServername !== "")) && {
+		(systemEmailTLSServername && systemEmailTLSServername !== "") && {
 			tls: {
-				...(systemEmailRejectUnauthorized !== undefined && {
-					rejectUnauthorized: systemEmailRejectUnauthorized,
-				}),
+				rejectUnauthorized: systemEmailRejectUnauthorized,
 				...(systemEmailTLSServername &&
 					systemEmailTLSServername !== "" && {
 						servername: systemEmailTLSServername,
 					}),
 			},
 		}),

Or if you want both fields to be conditional, remove the default for systemEmailRejectUnauthorized in the destructuring (line 45) and keep the !== undefined checks.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed5758f and c01bccb.

📒 Files selected for processing (1)
  • client/src/Pages/v1/Settings/SettingsEmail.jsx (1 hunks)

Copy link

@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: 0

♻️ Duplicate comments (3)
server/src/service/v1/infrastructure/emailService.js (3)

137-195: Stronger build-time validation is good; templateValue logging may be overly verbose

The additional checks for missing/non-function templates, empty MJML, and failed MJML→HTML conversion are solid and make failures much clearer. When the template isn’t a function, logging templateValue itself could dump large or noisy data; logging just its type or a summarized descriptor (e.g., constructor name) would keep logs leaner without losing signal.


235-250: From-address validation assumes an email-style value; ensure config matches that

Deriving fromAddress from systemEmailAddress || systemEmailUser and requiring an @ is reasonable if you always expect a real email in at least one of those fields. Please double-check that all supported SMTP configurations in your deployments set systemEmailAddress (or otherwise use an email-form username); otherwise, this guard will cause valid non-email usernames to be rejected.

Also applies to: 313-313


199-219: Empty-HTML fallback helps, but warning log still exposes PII

The guard for missing to/subject and the HTML fallback avoid sending completely empty emails, which aligns with the infra-alert objective. However, the warning on empty HTML currently logs full to and subject, which can leak PII into log streams; consider removing these fields or redacting/masking them before logging.

🧹 Nitpick comments (2)
server/src/service/v1/infrastructure/emailService.js (2)

47-110: Multi-path template resolution looks robust; consider making base path configurable

The multi-path search plus logging should resolve ENOENT issues across dev and Docker layouts, and the no-op fallback combined with later MJML checks prevents silent runtime crashes. As a future improvement, you could allow an env/config-provided base template directory to reduce the need to hardcode multiple paths and simplify maintenance.


299-326: Transporter verification is helpful; log more detail on failures

Verifying the transporter before sending is a good safety net, and returning false on failures keeps the contract simple. To make diagnosing connectivity/TLS/auth issues easier, consider including error.message (and perhaps host/port) in the verification warning log while still avoiding credentials, and optionally reusing a transporter instead of recreating and verifying it on every send if email volume grows.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c01bccb and fe27ce2.

📒 Files selected for processing (1)
  • server/src/service/v1/infrastructure/emailService.js (6 hunks)
🔇 Additional comments (2)
server/src/service/v1/infrastructure/emailService.js (2)

252-296: Conditional TLS wiring and top-level flags align with the stated behavior

Building a minimal base emailConfig and only attaching TLS-related options when systemEmailSecure is true should prevent TLS flags from being passed to providers like Resend, while still honoring explicitly configured TLS behavior when secure mode is enabled. The separation of top-level flags and tls sub-options looks consistent within this file.


1-330: Remember to re-run Prettier on this file to satisfy CI

CI previously failed on Prettier for this file; after your latest edits, please re-run the project’s Prettier command (likely with --write) so the formatting check passes.

@sameh0
Copy link
Author

sameh0 commented Nov 20, 2025

@gorkem-bwl I believe it's all looking good now, could you please run the check flow

@gorkem-bwl
Copy link
Contributor

@gorkem-bwl I believe it's all looking good now, could you please run the check flow

Sure running, lets see how it goes.

Copy link
Contributor

@Owaiseimdad Owaiseimdad left a comment

Choose a reason for hiding this comment

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

The PR includes good improvements but introduces several behavior changes that could affect production email delivery. Before merging, we should clarify the intention behind TLS changes, pooling removal, and silent template/HTML fallbacks. These should either be reverted for backward compatibility or documented as deliberate changes.

requireTLS: systemEmailRequireTLS,
servername: systemEmailTLSServername,
},
...(systemEmailSecure && {
Copy link
Contributor

Choose a reason for hiding this comment

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

@sameh0 --- thanks for the update! I had a question and a suggestion regarding the new logic that builds the transport config.

The current version uses multiple nested spread operators, and while it works, it becomes quite hard to read and maintain — especially for someone new who is trying to contribute. Nested conditional spreads are clever, but they make the intention of the code harder to understand at a glance.

Another point of confusion: systemEmailRequireTLS and systemEmailIgnoreTLS were previously under the tls object, but in the PR they are now moved to the top-level. Is there a specific reason for that? Just trying to understand the intention — since mixing some TLS fields at the root and others inside tls can make the structure inconsistent.

return this.compile(templateContent);
// Try multiple possible paths for template files
// to support both development and production environments
const possiblePaths = [
Copy link
Contributor

Choose a reason for hiding this comment

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

@sameh0 thanks for the contributions, I have some certain points here.

  1. Very long list of paths = harder to maintain
  2. Performance overhead for checking 6 paths per template

Why do we need all these fallback paths?
Can we standardize template paths instead of searching everywhere?

} catch (error) {
this.logger.error({
message: error.message,
message: `Failed to load template '${templateName}': ${error.message}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is good. Better clarity.

} = config;

// Validate from address
const fromAddress = systemEmailAddress || systemEmailUser;
Copy link
Contributor

Choose a reason for hiding this comment

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

@sameh0 any reason why we are considering the user and email address is same here??

}
};

sendEmail = async (to, subject, html, transportConfig) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

@sameh0 , honestly the sendEmail is becoming bigger and bigger now, harder to maintain then.

Possible to have a clean code, like:

`sendEmail = async (to, subject, html, transportConfig) => {
// 1. Validate incoming email data
html = this.validateEmailParams(to, subject, html);
if (html === false) return false;

// 2. Load config
const config = await this.getTransportConfig(transportConfig);

// 3. Validate from address
const from = this.validateFromAddress(config.systemEmailAddress, config.systemEmailUser);
if (!from) return false;

// 4. Build TLS options
const tlsRelated = this.buildTLSConfig({
	secure: config.systemEmailSecure,
	systemEmailIgnoreTLS: config.systemEmailIgnoreTLS,
	systemEmailRequireTLS: config.systemEmailRequireTLS,
	systemEmailRejectUnauthorized: config.systemEmailRejectUnauthorized,
	systemEmailTLSServername: config.systemEmailTLSServername,
});

// 5. Build full nodemailer config
const emailConfig = this.buildTransport({ ...config, ...tlsRelated });

// 6. Send
return await this.sendWithTransporter(from, to, subject, html, emailConfig);

};
`

This is just a snapshot which explains what I am thinking, clearly breaking the responsibilities and maintaining it will become much easier.

},
name: systemEmailConnectionHost || "localhost",
connectionTimeout: 5000,
pool: systemEmailPool,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you clarify the reasoning behind that?
pool controls whether Nodemailer keeps SMTP connections open and reuses them, which can significantly improve performance for multiple emails or batch sending.

to: to,
subject: subject,
});
html = "<p>Email content unavailable</p>";
Copy link
Contributor

Choose a reason for hiding this comment

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

why are we sending the email if the content is not available??

@sameh0
Copy link
Author

sameh0 commented Nov 20, 2025

@Owaiseimdad Thanks for your comments. Yes, it’s indeed getting complex. I do see some of the points you raised, and I’ll reconsider them. However, I just want to clarify that the current implementation on master is broken. Mine fixes it for some use cases, but it might break or at least limit other people’s setups.

The main thing I’m addressing is getting setups without TLS (like this one) to work. Still, I need to consider pooling and other things. I’ll rework it and get back to you.

@Owaiseimdad
Copy link
Contributor

@Owaiseimdad Thanks for your comments. Yes, it’s indeed getting complex. I do see some of the points you raised, and I’ll reconsider them. However, I just want to clarify that the current implementation on master is broken. Mine fixes it for some use cases, but it might break or at least limit other people’s setups.

The main thing I’m addressing is getting setups without TLS (like this one) to work. Still, I need to consider pooling and other things. I’ll rework it and get back to you.

Thanks for the contribution. And yes you are right, the code is getting bigger everyday. Let me know if you have anything in mind. I can jump on and contribute as well if required.

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.

Infrastructure alerts emails are empty

3 participants