-
-
Notifications
You must be signed in to change notification settings - Fork 615
Fix email template loading and conditional TLS configuration #3066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
ee0ad25
0b3e023
621d3c7
ed5758f
c01bccb
fe27ce2
515d6cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,16 +46,67 @@ class EmailService { | |
| */ | ||
| this.loadTemplate = (templateName) => { | ||
| try { | ||
| const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`); | ||
| const templateContent = this.fs.readFileSync(templatePath, "utf8"); | ||
| return this.compile(templateContent); | ||
| // Try multiple possible paths for template files | ||
| // to support both development and production environments | ||
| const possiblePaths = [ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`);
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sameh0 thanks for the contributions, I have some certain points here.
Why do we need all these fallback paths? |
||
| // 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(", ")}`); | ||
| } | ||
|
|
||
| this.logger.debug({ | ||
| message: `Loading template: ${templateName}`, | ||
| service: SERVICE_NAME, | ||
| method: "loadTemplate", | ||
| templatePath: templatePath, | ||
| }); | ||
|
|
||
| const compiled = this.compile(templateContent); | ||
|
|
||
| this.logger.debug({ | ||
| message: `Template loaded successfully: ${templateName}`, | ||
| service: SERVICE_NAME, | ||
| method: "loadTemplate", | ||
| }); | ||
| return compiled; | ||
| } catch (error) { | ||
| this.logger.error({ | ||
| message: error.message, | ||
| message: `Failed to load template '${templateName}': ${error.message}`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is good. Better clarity. |
||
| 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 () => ""; | ||
|
Comment on lines
+108
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
| } | ||
| }; | ||
|
|
||
|
|
@@ -83,20 +134,90 @@ class EmailService { | |
|
|
||
| buildEmail = async (template, context) => { | ||
| try { | ||
| if (!this.templateLookup[template]) { | ||
| this.logger.error({ | ||
| message: `Template '${template}' not found in templateLookup`, | ||
| service: SERVICE_NAME, | ||
| method: "buildEmail", | ||
| availableTemplates: Object.keys(this.templateLookup), | ||
| }); | ||
| throw new Error(`Template '${template}' not found`); | ||
| } | ||
| 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`); | ||
| } | ||
| const mjml = this.templateLookup[template](context); | ||
|
|
||
| // Check if MJML is empty (template failed to load) | ||
| if (!mjml || mjml.trim() === "") { | ||
| const msg = `Template '${template}' returned empty MJML content. Template may have failed to load.`; | ||
| this.logger.error({ | ||
| message: msg, | ||
| service: SERVICE_NAME, | ||
| method: "buildEmail", | ||
| template: template, | ||
| }); | ||
| throw new Error(msg); | ||
| } | ||
|
|
||
| const html = await this.mjml2html(mjml); | ||
|
|
||
| // Check if HTML is empty | ||
| if (!html || !html.html) { | ||
| const msg = `MJML conversion failed for template '${template}'. No HTML output.`; | ||
| this.logger.error({ | ||
| message: msg, | ||
| service: SERVICE_NAME, | ||
| method: "buildEmail", | ||
| template: template, | ||
| mjmlLength: mjml.length, | ||
| }); | ||
| throw new Error(msg); | ||
| } | ||
|
|
||
| return html.html; | ||
| } catch (error) { | ||
| this.logger.error({ | ||
| message: error.message, | ||
| message: `Failed to build email for template '${template}': ${error.message}`, | ||
| service: SERVICE_NAME, | ||
| method: "buildEmail", | ||
| template: template, | ||
| error: error.message, | ||
| stack: error.stack, | ||
| }); | ||
| throw error; | ||
| } | ||
| }; | ||
|
|
||
| sendEmail = async (to, subject, html, transportConfig) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { }; This is just a snapshot which explains what I am thinking, clearly breaking the responsibilities and maintaining it will become much easier. |
||
| // Validate required fields | ||
| if (!to || !subject) { | ||
| this.logger.error({ | ||
| message: "Invalid email parameters: missing 'to' or 'subject'", | ||
| service: SERVICE_NAME, | ||
| method: "sendEmail", | ||
| }); | ||
| return false; | ||
| } | ||
|
|
||
| // Validate HTML content | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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>";
} |
||
| 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>"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why are we sending the email if the content is not available?? |
||
| } | ||
|
|
||
| let config; | ||
| if (typeof transportConfig !== "undefined") { | ||
| config = transportConfig; | ||
|
|
@@ -107,17 +228,28 @@ class EmailService { | |
| systemEmailHost, | ||
| systemEmailPort, | ||
| systemEmailSecure, | ||
| systemEmailPool, | ||
| systemEmailUser, | ||
| systemEmailAddress, | ||
| systemEmailPassword, | ||
| systemEmailConnectionHost, | ||
| systemEmailTLSServername, | ||
| systemEmailRejectUnauthorized, | ||
| systemEmailIgnoreTLS, | ||
| systemEmailRequireTLS, | ||
| systemEmailRejectUnauthorized, | ||
| systemEmailTLSServername, | ||
| } = config; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Validate from address | ||
| const fromAddress = systemEmailAddress || systemEmailUser; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?? |
||
| if (!fromAddress || !fromAddress.includes("@")) { | ||
| this.logger.error({ | ||
| message: "Invalid from email address", | ||
| service: SERVICE_NAME, | ||
| method: "sendEmail", | ||
| }); | ||
| return false; | ||
| } | ||
|
|
||
| // Build base email config | ||
| const emailConfig = { | ||
| host: systemEmailHost, | ||
| port: Number(systemEmailPort), | ||
|
|
@@ -128,14 +260,40 @@ class EmailService { | |
| }, | ||
| name: systemEmailConnectionHost || "localhost", | ||
| connectionTimeout: 5000, | ||
| pool: systemEmailPool, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you clarify the reasoning behind that? |
||
| tls: { | ||
| rejectUnauthorized: systemEmailRejectUnauthorized, | ||
| ignoreTLS: systemEmailIgnoreTLS, | ||
| requireTLS: systemEmailRequireTLS, | ||
| servername: systemEmailTLSServername, | ||
| }, | ||
| }; | ||
|
|
||
| // Conditionally add TLS settings only if secure is enabled | ||
| if (systemEmailSecure) { | ||
| // Add top-level TLS options | ||
| if (systemEmailIgnoreTLS !== undefined) { | ||
| emailConfig.ignoreTLS = systemEmailIgnoreTLS; | ||
| } | ||
| if (systemEmailRequireTLS !== undefined) { | ||
| emailConfig.requireTLS = systemEmailRequireTLS; | ||
| } | ||
|
|
||
| const tlsSettings = {}; | ||
|
|
||
| // Only add TLS settings that are explicitly configured | ||
| // (rejectUnauthorized and servername go INSIDE the tls object) | ||
| 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), | ||
| }); | ||
| } | ||
| } | ||
| this.transporter = this.nodemailer.createTransport(emailConfig); | ||
|
|
||
| try { | ||
|
|
@@ -152,7 +310,7 @@ class EmailService { | |
| try { | ||
| const info = await this.transporter.sendMail({ | ||
| to: to, | ||
| from: systemEmailAddress, | ||
| from: fromAddress, | ||
| subject: subject, | ||
| html: html, | ||
| }); | ||
|
|
@@ -164,6 +322,7 @@ class EmailService { | |
| method: "sendEmail", | ||
| stack: error.stack, | ||
| }); | ||
| return false; | ||
| } | ||
| }; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
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.