Skip to content

Fix email template loading and conditional TLS configuration#3066

Closed
sameh0 wants to merge 9 commits intobluewave-labs:developfrom
sameh0:develop
Closed

Fix email template loading and conditional TLS configuration#3066
sameh0 wants to merge 9 commits intobluewave-labs:developfrom
sameh0:develop

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

    • More robust email template loading with environment-aware lookup, richer error logs, and clearer failures instead of silent no-ops.
    • Stronger validation for email content and sender address; improved transport verification and TLS handling to reduce send-time errors.
  • UI

    • Settings screen now builds and displays an incremental, conditional email configuration preview (TLS fields shown only when applicable).

✏️ Tip: You can customize this high-level summary in your review settings.

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

coderabbitai bot commented Nov 17, 2025

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

Walkthrough

Server email service: replaces hard-coded template path resolution with environment-driven discovery, adds template and input validation, improves MJML→HTML validation, centralizes TLS config building, verifies transporter before send, and returns explicit false on failures. Client settings UI now conditionally includes TLS fields in the preview JSON.

Changes

Cohort / File(s) Summary
Email service core
server/src/service/v1/infrastructure/emailService.js
Reworked template discovery to use EMAIL_TEMPLATE_PATH or CWD templates and probe prod/dist paths; preloads/compiles templates into templateLookup; added validateEmailParams, validateFromAddress, and buildTLSConfig; stronger MJML → HTML validation and richer error context; transporter.verify() before send; TLS options applied only when systemEmailSecure true; explicit false returns on verification/send failures.
Client settings UI (preview)
client/src/Pages/v1/Settings/SettingsEmail.jsx
Replaced static JSON preview with buildEmailConfigPreview() builder that conditionally includes ignoreTLS, requireTLS, and nested tls (rejectUnauthorized, servername) only when systemEmailSecure is true and values exist; preserves prior display behavior otherwise.

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 rgba(240,248,255,0.8)
    Note over EmailSvc: validate inputs & from address
    EmailSvc->>EmailSvc: validateEmailParams(), validateFromAddress()
    alt invalid input
        EmailSvc-->>Caller: return false
    end
    end

    rect rgba(255,250,240,0.8)
    Note over Loader: template discovery (env / CWD / prod-dist)
    EmailSvc->>Loader: loadTemplate(templateName)
    Loader->>Loader: probe paths, compile first-found
    alt found
        Loader-->>EmailSvc: compiled template
    else not found
        Loader-->>EmailSvc: no-op template (or throw)
    end
    end

    rect rgba(240,255,240,0.8)
    Note over Builder: render & validate MJML → HTML
    EmailSvc->>Builder: buildEmail(compiledTemplate, data)
    Builder->>MJML: compile MJML to HTML
    alt empty/invalid HTML
        Builder-->>EmailSvc: throw / return error
    else valid HTML
        Builder-->>EmailSvc: HTML ready
    end
    end

    rect rgba(255,245,240,0.8)
    Note over SMTP: TLS config, verify and send
    EmailSvc->>EmailSvc: buildTLSConfig(config)
    EmailSvc->>SMTP: transporter.verify()
    alt verify fails
        SMTP-->>EmailSvc: error
        EmailSvc-->>Caller: return false
    else verify OK
        EmailSvc->>SMTP: sendMail({from, to, subject, html})
        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

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇
I hopped through folders, sniffed each name,
Compiled the MJML, chased the flame.
I checked the TLS, then gave a nod,
Verified the post, delivered to prod.
Alerts now land warm — hooray, little squad. ✉️🎉

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: fixing email template loading and making TLS configuration conditional, which aligns with the core objectives of the PR.
Description check ✅ Passed The PR description is comprehensive, covering changes made, issue references, and all checklist items properly marked as completed.
Linked Issues check ✅ Passed The code changes address all objectives from issue #3054: template loading is fixed via environment-driven paths, TLS is now conditional, validation prevents empty content, and improved error logging replaces cryptic failures.
Out of Scope Changes check ✅ Passed All changes in both emailService.js and SettingsEmail.jsx are directly related to the linked issue objectives regarding email template loading and conditional TLS configuration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

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.

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)

286-303: Consider refactoring nested spreads for better readability.

As noted by a previous reviewer, the deeply nested conditional spread operators make this code difficult to read and maintain. Consider building the configuration object imperatively or extracting helper functions to improve clarity.

🔎 Example refactor
// Build config object step by step
const config = {
	host: systemEmailHost,
	port: systemEmailPort,
	secure: systemEmailSecure,
	auth: {
		user: systemEmailUser || systemEmailAddress,
		pass: "<your_password>",
	},
	name: systemEmailConnectionHost || "localhost",
	pool: systemEmailPool,
};

// Add TLS fields only when secure mode is enabled
if (systemEmailSecure) {
	if (systemEmailIgnoreTLS !== undefined) {
		config.ignoreTLS = systemEmailIgnoreTLS;
	}
	if (systemEmailRequireTLS !== undefined) {
		config.requireTLS = systemEmailRequireTLS;
	}
	
	// Build tls object if needed
	const tlsConfig = {};
	if (systemEmailRejectUnauthorized !== undefined) {
		tlsConfig.rejectUnauthorized = systemEmailRejectUnauthorized;
	}
	if (systemEmailTLSServername && systemEmailTLSServername !== "") {
		tlsConfig.servername = systemEmailTLSServername;
	}
	if (Object.keys(tlsConfig).length > 0) {
		config.tls = tlsConfig;
	}
}

// Then use in JSON.stringify
{JSON.stringify(config, null, 2)}

This approach is more explicit about what conditions include which fields, making the logic easier to follow and debug.

📜 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 fe27ce2 and c51e00a.

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

Comment on lines +286 to +303
...(systemEmailSecure && {
...(systemEmailIgnoreTLS && { ignoreTLS: systemEmailIgnoreTLS }),
...(systemEmailRequireTLS && { requireTLS: systemEmailRequireTLS }),
}),
...(systemEmailSecure &&
(systemEmailRejectUnauthorized !== undefined ||
(systemEmailTLSServername &&
systemEmailTLSServername !== "")) && {
tls: {
...(systemEmailRejectUnauthorized !== undefined && {
rejectUnauthorized: systemEmailRejectUnauthorized,
}),
...(systemEmailTLSServername &&
systemEmailTLSServername !== "" && {
servername: systemEmailTLSServername,
}),
},
}),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Logic flaws in conditional checks due to default values.

Several issues with the conditional logic:

  1. Line 295: systemEmailRejectUnauthorized !== undefined is ineffective because systemEmailRejectUnauthorized is destructured with a default value of true (line 45), so it will never be undefined. This means rejectUnauthorized will always be included in the tls object whenever the outer condition is met, regardless of whether it was explicitly set.

  2. Lines 287-288: The truthy checks (systemEmailIgnoreTLS && and systemEmailRequireTLS &&) mean these fields are only included when they're true. Explicit false values won't be represented in the JSON output. If the server distinguishes between "field absent" and "field explicitly false," this logic is incorrect.

🔎 Suggested fix

To properly handle these cases, remove the default values for TLS boolean fields so you can detect whether they were explicitly set:

 const {
 	systemEmailHost = "",
 	systemEmailPort = "",
 	systemEmailSecure = false,
 	systemEmailPool = false,
 	systemEmailUser = "",
 	systemEmailAddress = "",
 	systemEmailPassword = "",
 	systemEmailTLSServername = "",
 	systemEmailConnectionHost = "localhost",
-	systemEmailIgnoreTLS = false,
-	systemEmailRequireTLS = false,
-	systemEmailRejectUnauthorized = true,
+	systemEmailIgnoreTLS,
+	systemEmailRequireTLS,
+	systemEmailRejectUnauthorized,
 } = settingsData?.settings || {};

Then update the conditions to check for !== undefined instead of truthy values:

 ...(systemEmailSecure && {
-	...(systemEmailIgnoreTLS && { ignoreTLS: systemEmailIgnoreTLS }),
-	...(systemEmailRequireTLS && { requireTLS: systemEmailRequireTLS }),
+	...(systemEmailIgnoreTLS !== undefined && { ignoreTLS: systemEmailIgnoreTLS }),
+	...(systemEmailRequireTLS !== undefined && { requireTLS: systemEmailRequireTLS }),
 }),

This ensures that explicit false values are included when set, while absent values remain absent.

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

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

111-119: Avoid logging templateValue in error context.

Logging templateValue (line 116) may expose internal implementation details or template content in logs. Since you already log the template name and the type, which are sufficient for debugging this issue, consider removing templateValue from the log.

🔎 Suggested fix
 			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 c51e00a and 5aa348e.

📒 Files selected for processing (2)
  • client/src/Pages/v1/Settings/SettingsEmail.jsx
  • server/src/service/v1/infrastructure/emailService.js
🔇 Additional comments (8)
client/src/Pages/v1/Settings/SettingsEmail.jsx (2)

104-143: Well-structured refactor for code preview readability.

The new buildEmailConfigPreview function improves maintainability by replacing nested conditional spreads with clear, step-by-step object construction. This directly addresses earlier feedback about complexity.

Minor note: The checks on lines 122-127 only include ignoreTLS/requireTLS when they're truthy (true). If a user explicitly sets these to false, they won't appear in the preview. For a display-only preview, this is acceptable since showing only enabled settings is arguably cleaner. If you want to show explicitly-set false values too, you'd need to track whether the user modified these settings.


315-315: Clean integration of the preview builder.

The JSON output now uses the new builder function, producing a cleaner preview that correctly reflects TLS settings only when secure mode is enabled.

server/src/service/v1/infrastructure/emailService.js (6)

41-76: Good simplification of template loading.

The refactored approach using EMAIL_TEMPLATE_PATH environment variable with a sensible default addresses the earlier concern about maintaining 6 different paths. Throwing the error instead of returning an empty function ensures failures are visible rather than producing empty emails silently.


163-190: Clean validation helper.

The validateEmailParams method properly validates required fields without logging PII, addressing the earlier concern about email addresses in logs.


192-208: Correct validation targeting.

Using only systemEmailAddress for validation (not systemEmailUser) correctly addresses the earlier concern about SMTP configurations where the username differs from the email address.


210-263: Well-designed TLS configuration builder.

This helper correctly addresses multiple past review concerns:

  1. TLS settings are only applied when systemEmailSecure is enabled
  2. ignoreTLS/requireTLS are properly placed at the top level for nodemailer
  3. rejectUnauthorized/servername are correctly nested in the tls object

The inline comments documenting the purpose of each option are helpful for future maintainers.


265-341: Clean refactored sendEmail implementation.

The method now follows a clear flow: validate → configure → build TLS → send. The helper method extractions improve readability and testability.

One consideration: the transporter is recreated on every sendEmail call (line 310). For low-volume email sending this is fine, but if high-throughput email sending becomes a requirement, you may want to cache and reuse the transporter when the configuration hasn't changed.


312-322: Appropriate logging level for verification failure.

Using warn for transporter verification failure is a reasonable choice since this could be a transient connectivity issue rather than a configuration error. The error message is logged without exposing sensitive configuration details.

@gorkem-bwl
Copy link
Contributor

Closing this one and using the relevant code to create #3066

@gorkem-bwl gorkem-bwl closed this Jan 16, 2026
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