diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx index 88f8f98c1..e7ae88154 100644 --- a/client/src/Pages/Settings/SettingsEmail.jsx +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -12,6 +12,7 @@ import { useTranslation } from "react-i18next"; import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; import { useSendTestEmail } from "../../Hooks/useSendTestEmail"; import { createToast } from "../../Utils/toastUtils"; +import { Switch } from "@mui/material"; const SettingsEmail = ({ isAdmin, @@ -25,6 +26,21 @@ const SettingsEmail = ({ const { t } = useTranslation(); const theme = useTheme(); + // Destructure settings with default values + const { + systemEmailHost = "", + systemEmailPort = "", + systemEmailSecure = false, + systemEmailPool = false, + systemEmailUser = "", + systemEmailAddress = "", + systemEmailPassword = "", + systemEmailTLSServername = "", + systemEmailConnectionHost = "localhost", + systemEmailIgnoreTLS = false, + systemEmailRequireTLS = false, + systemEmailRejectUnauthorized = true, + } = settingsData?.settings || {}; // Local state const [password, setPassword] = useState(""); const [hasBeenReset, setHasBeenReset] = useState(false); @@ -47,18 +63,32 @@ const SettingsEmail = ({ const handleSendTestEmail = () => { // Collect current form values const emailConfig = { - systemEmailHost: settingsData?.settings?.systemEmailHost, - systemEmailPort: settingsData?.settings?.systemEmailPort, - systemEmailUser: settingsData?.settings?.systemEmailUser, - systemEmailAddress: settingsData?.settings?.systemEmailAddress, - systemEmailPassword: password || settingsData?.settings?.systemEmailPassword, - systemEmailConnectionHost: settingsData?.settings?.systemEmailConnectionHost, + systemEmailHost, + systemEmailPort, + systemEmailSecure, + systemEmailPool, + systemEmailUser, + systemEmailAddress, + systemEmailPassword: password || systemEmailPassword, + systemEmailTLSServername, + systemEmailConnectionHost, + systemEmailIgnoreTLS, + systemEmailRequireTLS, + systemEmailRejectUnauthorized, }; // Basic validation - if (!emailConfig.systemEmailHost || !emailConfig.systemEmailPort) { + if ( + !emailConfig.systemEmailHost || + !emailConfig.systemEmailPort || + !emailConfig.systemEmailAddress || + !emailConfig.systemEmailPassword + ) { createToast({ - body: t("settingsEmailRequiredFields", "Email host and port are required"), + body: t( + "settingsEmailRequiredFields", + "Email address, host, port and password are required" + ), variant: "error", }); return; @@ -90,7 +120,7 @@ const SettingsEmail = ({ @@ -100,7 +130,7 @@ const SettingsEmail = ({ name="systemEmailPort" placeholder="425" type="number" - value={settingsData?.settings?.systemEmailPort ?? ""} + value={systemEmailPort} onChange={handleChange} /> @@ -109,7 +139,7 @@ const SettingsEmail = ({ @@ -118,7 +148,7 @@ const SettingsEmail = ({ @@ -156,23 +186,75 @@ const SettingsEmail = ({ )} - {t("settingsEmailConnectionHost")} + {t("settingsEmailTLSServername")} - + {t("settingsEmailConnectionHost")} + + + + {t("settingsEmailSecure")} + + {t("settingsEmailPool")} + + {t("settingsEmailIgnoreTLS")} + + {t("settingsEmailRequireTLS")} + + {t("settingsEmailRejectUnauthorized")} + + + + {systemEmailHost && + systemEmailPort && + systemEmailAddress && + systemEmailPassword && ( + + )} diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index a3cd88fca..804ca48c7 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -56,17 +56,27 @@ const Settings = () => { // Handlers const handleChange = async (e) => { - const { name, value } = e.target; + const { name, value, checked } = e.target; // Special case for showURL until handled properly in the backend if (name === "showURL") { dispatch(setShowURL(value)); return; } + let newValue; + if ( + name === "systemEmailIgnoreTLS" || + name === "systemEmailRequireTLS" || + name === "systemEmailRejectUnauthorized" || + name === "systemEmailSecure" || + name === "systemEmailPool" + ) { + newValue = checked; + } // Build next state early const newSettingsData = { ...settingsData, - settings: { ...settingsData.settings, [name]: value }, + settings: { ...settingsData.settings, [name]: newValue ?? value }, }; // Validate diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index ecde44a9e..3059462f9 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -973,10 +973,12 @@ class NetworkService { systemEmailPort: emailConfig.systemEmailPort, systemEmailAddress: emailConfig.systemEmailAddress, systemEmailPassword: emailConfig.systemEmailPassword, - // Only include these if they are present - ...(emailConfig.systemEmailConnectionHost && { - systemEmailConnectionHost: emailConfig.systemEmailConnectionHost, - }), + systemEmailSecure: emailConfig.systemEmailSecure, + systemEmailPool: emailConfig.systemEmailPool, + systemEmailIgnoreTLS: emailConfig.systemEmailIgnoreTLS, + systemEmailRequireTLS: emailConfig.systemEmailRequireTLS, + systemEmailRejectUnauthorized: emailConfig.systemEmailRejectUnauthorized, + systemEmailTLSServername: emailConfig.systemEmailTLSServername, ...(emailConfig.systemEmailUser && { systemEmailUser: emailConfig.systemEmailUser, }), diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 043e88f50..f1811a124 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -290,10 +290,16 @@ const settingsValidation = joi.object({ timezone: joi.string().allow("").optional(), systemEmailHost: joi.string().allow(""), systemEmailPort: joi.number().allow(null, ""), + systemEmailSecure: joi.boolean().optional(), + systemEmailPool: joi.boolean().optional(), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), systemEmailUser: joi.string().allow(""), - systemEmailConnectionHost: joi.string().allow(""), + systemEmailConnectionHost: joi.string().allow("").optional(), + systemEmailTLSServername: joi.string().allow(""), + systemEmailIgnoreTLS: joi.boolean(), + systemEmailRequireTLS: joi.boolean(), + systemEmailRejectUnauthorized: joi.boolean(), }); const dayjsValidator = (value, helpers) => { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index bc6c8bbb9..2791dd3e7 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -681,12 +681,18 @@ }, "settingsEmail": "Email", "settingsEmailDescription": "Configure the email settings for your system. This is used to send notifications and alerts.", - "settingsEmailHost": "Email host - Hostname or IP address of the SMTP server", + "settingsEmailHost": "Email host - Hostname or IP address to connect to", "settingsEmailPort": "Email port - Port to connect to", "settingsEmailAddress": "Email address - Used for authentication", "settingsEmailPassword": "Email password - Password for authentication", "settingsEmailUser": "Email user - Username for authentication, overrides email address if specified", "settingsEmailFieldResetLabel": "Password is set. Click Reset to change it.", + "settingsEmailTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP", + "settingsEmailIgnoreTLS": "Ignore TLS - Disable STARTTLS", + "settingsEmailRequireTLS": "Require TLS - Force STARTTLS", + "settingsEmailRejectUnauthorized": "Reject Unauthorized", + "settingsEmailSecure": "Secure - Use SSL", + "settingsEmailPool": "Pool - Enable connection pooling", "state": "State", "statusBreadCrumbsStatusPages": "Status Pages", "statusBreadCrumbsDetails": "Details", @@ -716,7 +722,7 @@ "settingsTestEmailFailed": "Failed to send test email", "settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}", "settingsTestEmailUnknownError": "Unknown error", - "settingsEmailRequiredFields": "Email host and port are required", + "settingsEmailRequiredFields": "Email address, host, port and password are required", "statusMsg": { "paused": "Monitoring is paused.", "up": "Your site is up.", diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index 58e8ce9f4..d1010c093 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -74,6 +74,12 @@ class SettingsController { systemEmailPassword, systemEmailUser, systemEmailConnectionHost, + systemEmailSecure, + systemEmailPool, + systemEmailIgnoreTLS, + systemEmailRequireTLS, + systemEmailRejectUnauthorized, + systemEmailTLSServername, } = req.body; const subject = this.stringService.testEmailSubject; @@ -91,6 +97,12 @@ class SettingsController { systemEmailAddress, systemEmailPassword, systemEmailConnectionHost, + systemEmailSecure, + systemEmailPool, + systemEmailIgnoreTLS, + systemEmailRequireTLS, + systemEmailRejectUnauthorized, + systemEmailTLSServername, } ); diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js index c47de06fa..cd63118e9 100755 --- a/server/db/models/AppSettings.js +++ b/server/db/models/AppSettings.js @@ -32,6 +32,29 @@ const AppSettingsSchema = mongoose.Schema( type: String, default: "localhost", }, + systemEmailTLSServername: { + type: String, + }, + systemEmailSecure: { + type: Boolean, + default: false, + }, + systemEmailPool: { + type: Boolean, + default: false, + }, + systemEmailIgnoreTLS: { + type: Boolean, + default: false, + }, + systemEmailRequireTLS: { + type: Boolean, + default: false, + }, + systemEmailRejectUnauthorized: { + type: Boolean, + default: true, + }, singleton: { type: Boolean, required: true, diff --git a/server/service/emailService.js b/server/service/emailService.js index 035cc6505..09123ef93 100755 --- a/server/service/emailService.js +++ b/server/service/emailService.js @@ -101,39 +101,37 @@ class EmailService { const { systemEmailHost, systemEmailPort, + systemEmailSecure, + systemEmailPool, systemEmailUser, systemEmailAddress, systemEmailPassword, systemEmailConnectionHost, + systemEmailTLSServername, + systemEmailIgnoreTLS, + systemEmailRequireTLS, + systemEmailRejectUnauthorized, } = config; - const baseEmailConfig = { + const emailConfig = { host: systemEmailHost, - port: systemEmailPort, - secure: true, + port: Number(systemEmailPort), + secure: systemEmailSecure, auth: { user: systemEmailUser || systemEmailAddress, pass: systemEmailPassword, }, + name: systemEmailConnectionHost || "localhost", connectionTimeout: 5000, + pool: systemEmailPool, + tls: { + rejectUnauthorized: systemEmailRejectUnauthorized, + ignoreTLS: systemEmailIgnoreTLS, + requireTLS: systemEmailRequireTLS, + servername: systemEmailTLSServername, + }, }; - const isSmtps = Number(systemEmailPort) === 465; - - const emailConfig = !isSmtps - ? { - ...baseEmailConfig, - name: systemEmailConnectionHost || "localhost", - secure: false, - pool: true, - tls: { rejectUnauthorized: false }, - } - : baseEmailConfig; - - if (!isSmtps) { - delete emailConfig.auth; - } - this.transporter = this.nodemailer.createTransport(emailConfig); const buildHtml = async (template, context) => { diff --git a/server/validation/joi.js b/server/validation/joi.js index 622cb9148..5eb24e758 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -410,7 +410,13 @@ const updateAppSettingsBodyValidation = joi.object({ systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), systemEmailUser: joi.string().allow(""), - systemEmailConnectionHost: joi.string().allow(""), + systemEmailConnectionHost: joi.string().allow("").optional(), + systemEmailTLSServername: joi.string().allow("").optional(), + systemEmailSecure: joi.boolean(), + systemEmailPool: joi.boolean(), + systemEmailIgnoreTLS: joi.boolean(), + systemEmailRequireTLS: joi.boolean(), + systemEmailRejectUnauthorized: joi.boolean(), }); //**************************************** @@ -612,10 +618,16 @@ const sendTestEmailBodyValidation = joi.object({ to: joi.string().required(), systemEmailHost: joi.string(), systemEmailPort: joi.number(), + systemEmailSecure: joi.boolean(), + systemEmailPool: joi.boolean(), systemEmailAddress: joi.string(), systemEmailPassword: joi.string(), systemEmailUser: joi.string(), - systemEmailConnectionHost: joi.string(), + systemEmailConnectionHost: joi.string().allow("").optional(), + systemEmailIgnoreTLS: joi.boolean(), + systemEmailRequireTLS: joi.boolean(), + systemEmailRejectUnauthorized: joi.boolean(), + systemEmailTLSServername: joi.string().allow("").optional(), }); export {