From 3fcb7bf181ce30002d42c42f9296fa4103585e8b Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 16 Oct 2023 16:16:49 +0200 Subject: [PATCH] Feature: SMTP-templating of `customBody` and `customHeader` via liquidjs (#3414) * replaced the regex replacement engine with `Liquid` * added custom bodys * fixed a typo * formatting fixes * switched all template-variables to be camelCase --- server/notification-providers/smtp.js | 121 ++++++++++++++------------ src/components/notifications/SMTP.vue | 33 ++++--- src/lang/en.json | 12 +++ 3 files changed, 100 insertions(+), 66 deletions(-) diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js index 170726250..0e761bc04 100644 --- a/server/notification-providers/smtp.js +++ b/server/notification-providers/smtp.js @@ -1,6 +1,7 @@ const nodemailer = require("nodemailer"); const NotificationProvider = require("./notification-provider"); const { DOWN } = require("../../src/util"); +const { Liquid } = require("liquidjs"); class SMTP extends NotificationProvider { @@ -39,76 +40,86 @@ class SMTP extends NotificationProvider { pass: notification.smtpPassword, }; } - // Lets start with default subject and empty string for custom one + + // default values in case the user does not want to template let subject = msg; - - // Change the subject if: - // - The msg ends with "Testing" or - // - Actual Up/Down Notification - if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) { - let customSubject = ""; - - // Our subject cannot end with whitespace it's often raise spam score - // Once I got "Cannot read property 'trim' of undefined", better be safe than sorry - if (notification.customSubject) { - customSubject = notification.customSubject.trim(); - } - - // If custom subject is not empty, change subject for notification - if (customSubject !== "") { - - // Replace "MACROS" with corresponding variable - let replaceName = new RegExp("{{NAME}}", "g"); - let replaceHostnameOrURL = new RegExp("{{HOSTNAME_OR_URL}}", "g"); - let replaceStatus = new RegExp("{{STATUS}}", "g"); - - // Lets start with dummy values to simplify code - let monitorName = "Test"; - let monitorHostnameOrURL = "testing.hostname"; - let serviceStatus = "⚠️ Test"; - - if (monitorJSON !== null) { - monitorName = monitorJSON["name"]; - - if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") { - monitorHostnameOrURL = monitorJSON["url"]; - } else { - monitorHostnameOrURL = monitorJSON["hostname"]; - } - } - - if (heartbeatJSON !== null) { - serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up"; - } - - // Break replace to one by line for better readability - customSubject = customSubject.replace(replaceStatus, serviceStatus); - customSubject = customSubject.replace(replaceName, monitorName); - customSubject = customSubject.replace(replaceHostnameOrURL, monitorHostnameOrURL); - - subject = customSubject; - } - } - - let transporter = nodemailer.createTransport(config); - - let bodyTextContent = msg; + let body = msg; if (heartbeatJSON) { - bodyTextContent = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`; + body = `${msg}\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`; + } + // subject and body are templated + if ((monitorJSON && heartbeatJSON) || msg.endsWith("Testing")) { + // cannot end with whitespace as this often raises spam scores + const customSubject = notification.customSubject?.trim() || ""; + const customBody = notification.customBody?.trim() || ""; + + const context = this.generateContext(msg, monitorJSON, heartbeatJSON); + const engine = new Liquid(); + if (customSubject !== "") { + const tpl = engine.parse(customSubject); + subject = await engine.render(tpl, context); + } + if (customBody !== "") { + const tpl = engine.parse(customBody); + body = await engine.render(tpl, context); + } } // send mail with defined transport object + let transporter = nodemailer.createTransport(config); await transporter.sendMail({ from: notification.smtpFrom, cc: notification.smtpCC, bcc: notification.smtpBCC, to: notification.smtpTo, subject: subject, - text: bodyTextContent, + text: body, }); return "Sent Successfully."; } + + /** + * Generate context for LiquidJS + * @param {string} msg the message that will be included in the context + * @param {?object} monitorJSON Monitor details (For Up/Down/Cert-Expiry only) + * @param {?object} heartbeatJSON Heartbeat details (For Up/Down only) + * @returns {{STATUS: string, status: string, HOSTNAME_OR_URL: string, hostnameOrUrl: string, NAME: string, name: string, monitorJSON: ?object, heartbeatJSON: ?object, msg: string}} the context + */ + generateContext(msg, monitorJSON, heartbeatJSON) { + // Let's start with dummy values to simplify code + let monitorName = "Monitor Name not available"; + let monitorHostnameOrURL = "testing.hostname"; + + if (monitorJSON !== null) { + monitorName = monitorJSON["name"]; + + if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") { + monitorHostnameOrURL = monitorJSON["url"]; + } else { + monitorHostnameOrURL = monitorJSON["hostname"]; + } + } + + let serviceStatus = "⚠️ Test"; + if (heartbeatJSON !== null) { + serviceStatus = (heartbeatJSON["status"] === DOWN) ? "🔴 Down" : "✅ Up"; + } + return { + // for v1 compatibility, to be removed in v3 + "STATUS": serviceStatus, + "NAME": monitorName, + "HOSTNAME_OR_URL": monitorHostnameOrURL, + + // variables which are officially supported + "status": serviceStatus, + "name": monitorName, + "hostnameOrURL": monitorHostnameOrURL, + monitorJSON, + heartbeatJSON, + msg, + }; + } } module.exports = SMTP; diff --git a/src/components/notifications/SMTP.vue b/src/components/notifications/SMTP.vue index 1c48313cc..e2ac77713 100644 --- a/src/components/notifications/SMTP.vue +++ b/src/components/notifications/SMTP.vue @@ -59,6 +59,28 @@ +

+ + {{ $t("documentation") }} + + {{name}}: {{ $t("emailTemplateServiceName") }}
+ {{msg}}: {{ $t("emailTemplateMsg") }}
+ {{status}}: {{ $t("emailTemplateStatus") }}
+ {{heartbeatJSON}}: {{ $t("emailTemplateHeartbeatJSON") }}{{ $t("emailTemplateLimitedToUpDownNotification") }}
+ {{monitorJSON}}: {{ $t("emailTemplateMonitorJSON") }} {{ $t("emailTemplateLimitedToUpDownNotification") }}
+ {{hostnameOrURL}}: {{ $t("emailTemplateHostnameOrURL") }}
+

+
+ + +
{{ $t("leave blank for default subject") }}
+
+
+ + +
{{ $t("leave blank for default body") }}
+
+ {{ $t("documentation") }} @@ -89,17 +111,6 @@ - -
- - -
- (leave blank for default one)
- {{NAME}}: Service Name
- {{HOSTNAME_OR_URL}}: Hostname or URL
- {{STATUS}}: Status
-
-
diff --git a/src/lang/en.json b/src/lang/en.json index f5ebea2a5..e1b390fa8 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -489,7 +489,19 @@ "secureOptionTLS": "TLS (465)", "Ignore TLS Error": "Ignore TLS Error", "From Email": "From Email", + "emailCustomisableContent": "Customisable content", + "smtpLiquidIntroduction": "The following two fields are templatable via the Liquid templating Language. Please refer to the {0} for usage instructions. These are the available variables:", "emailCustomSubject": "Custom Subject", + "leave blank for default subject": "leave blank for default subject", + "emailCustomBody": "Custom Body", + "leave blank for default body": "leave blank for default body", + "emailTemplateServiceName": "Service Name", + "emailTemplateHostnameOrURL": "Hostname or URL", + "emailTemplateStatus": "Status", + "emailTemplateMonitorJSON": "object describing the monitor", + "emailTemplateHeartbeatJSON": "object describing the heartbeat", + "emailTemplateMsg": "message of the notification", + "emailTemplateLimitedToUpDownNotification": "only available for UP/DOWN heartbeats, otherwise null", "To Email": "To Email", "smtpCC": "CC", "smtpBCC": "BCC",