diff --git a/social/email/61-email.html b/social/email/61-email.html index 42cccb5f..304be8b5 100644 --- a/social/email/61-email.html +++ b/social/email/61-email.html @@ -417,7 +417,7 @@ @@ -478,6 +479,7 @@ keyFile: { value: "" }, mtausers: { value: [] }, auth: { value: false }, + authType: { value: false }, expert: { value: '{"logger":false}' } }, inputs: 0, @@ -491,7 +493,8 @@ return this.name ? "node_label_italic" : ""; }, oneditprepare: function () { - let node = this; + const node = this; + // Certificate settings $("#node-input-secure").change(secVisibility); $("#node-input-starttls").change(secVisibility); @@ -505,6 +508,21 @@ } } // User Management + const noneType = { value: 'none', label: node._('email.label.noAuth'), hasValue: false } + const builtInType = { value: 'built-in', label: node._('email.label.auth'), hasValue: false } + const AUTH_TYPES = ['none', 'built-in', "flow", "global", "jsonata"] + if (!AUTH_TYPES.includes(node.authType)) { + // in-place upgrade + node.authType = (node.auth === "true" || node.auth === true) ? "built-in" : "none"; + $('#node-input-authType').val(node.authType); + } + const $authTypedInput = $('#node-input-auth').typedInput({ + default: 'none', + typeField: $('#node-input-authType'), + types: [noneType, builtInType, 'flow', 'global', 'jsonata'] + }) + $authTypedInput.change(builtInUsersVisibility); + let cacheItemCount = 0; if (node.mtausers && node.mtausers.length > 0) { cacheItemCount = node.mtausers.length; @@ -524,7 +542,7 @@ id: "node-input-email-users-name" + id, class: "userName", type: "text", - style: "margin-left:5px;width:100px;", + style: "margin-left:5px;width:42%;", placeholder: "name" }).appendTo(row); @@ -532,7 +550,7 @@ id: "node-input-email-users-password" + id, class: "userPassword", type: "password", - style: "margin: 0 auto;width:50%;min-width:20px;margin-left:5px", + style: "margin: 0 auto;width:42%;margin-left:5px", placeholder: "password" }).appendTo(row); @@ -561,7 +579,24 @@ $("#node-input-email-users-container").append(container); } - + function builtInUsersVisibility() { + const authType = $authTypedInput.typedInput('type'); + if (authType === 'built-in') { + // show user list + $("#node-input-email-users-add").show(); + $("#node-input-email-users-container-div").show(); + $("#node-custom-auth-tip").hide(); + } else { + $("#node-input-email-users-add").hide(); + $("#node-input-email-users-container-div").hide(); + if (authType === 'none') { + $("#node-custom-auth-tip").hide(); + } else { + // show user tip for flow/global/jsonata + $("#node-custom-auth-tip").show(); + } + } + } $("#node-input-email-users-container").sortable({ axis: "y", handle: ".node-input-email-users-handle", @@ -579,20 +614,14 @@ $("#node-input-email-users-container-div").get(0).scrollHeight ); }); - $("#node-input-auth").change(function () { - if ($("#node-input-auth").is(":checked")) { - $("#node-input-email-users-add").show(); - $("#node-input-email-users-container-div").show(); - } else { - $("#node-input-email-users-add").hide(); - $("#node-input-email-users-container-div").hide(); - } - }); + // Expert settings $("#node-input-expert").typedInput({ type: "json", types: ["json"] }) + + builtInUsersVisibility(); }, oneditsave: function () { let node = this; diff --git a/social/email/61-email.js b/social/email/61-email.js index 90a52fa0..b423d0a8 100644 --- a/social/email/61-email.js +++ b/social/email/61-email.js @@ -631,6 +631,27 @@ module.exports = function(RED) { this.keyFile = n.keyFile; this.mtausers = n.mtausers; this.auth = n.auth; + /** @type {"none"|"built-in"|"flow"|"global"|"jsonata"} **/ + this.authType = n.authType; + const AUTH_TYPES = ['none', 'built-in', "flow", "global", "jsonata"] + if (!AUTH_TYPES.includes(this.authType)) { + // in-place upgrade + this.authType = (n.auth === "true" || n.auth === true) ? "built-in" : "none"; + } + + // basic user list duck type validation + const validateUserList = (users) => { + if (!Array.isArray(users) || users.length === 0) { + return false; + } + for (const user of users) { + if (!user.name || typeof user.password !== "string") { + return false; + } + } + return true; + } + try { this.options = JSON.parse(n.expert); } catch (error) { @@ -650,7 +671,7 @@ module.exports = function(RED) { if (!node.starttls) { node.options.disabledCommands.push("STARTTLS"); } - if (!node.auth) { + if (node.authType === "none") { node.options.disabledCommands.push("AUTH"); } @@ -686,14 +707,34 @@ module.exports = function(RED) { }); } - node.options.onAuth = function (auth, session, callback) { - let id = node.mtausers.findIndex(function (item) { - return item.name === auth.username; - }); - if (id >= 0 && node.mtausers[id].password === auth.password) { - callback(null, { user: id + 1 }); - } else { - callback(new Error("Invalid username or password")); + node.options.onAuth = async function (auth, session, callback) { + try { + if (node.authType === "none") { + return; // auth not enabled - should not reach here + } + let userList; + if (node.authType === "built-in") { + // users defined in UI + userList = node.mtausers; + } else { + // users defined in flow, global, or jsonata + userList = await asyncEvaluateNodeProperty(RED, node.auth, node.authType, node, {}); + } + + if (!validateUserList(userList)) { + callback(new Error("Invalid user list")); + } + + let id = userList.findIndex(function (item) { + return item.name === auth.username; + }); + if (id >= 0 && userList[id].password === auth.password) { + callback(null, { user: id + 1 }); + } else { + callback(new Error("Invalid username or password")); + } + } catch (error) { + callback(new Error("Invalid user list")); } } @@ -711,4 +752,16 @@ module.exports = function(RED) { } RED.nodes.registerType("e-mail mta",EmailMtaNode); + function asyncEvaluateNodeProperty (RED, value, type, node, msg) { + return new Promise(function (resolve, reject) { + RED.util.evaluateNodeProperty(value, type, node, msg, function (e, r) { + if (e) { + reject(e) + } else { + resolve(r) + } + }) + }) + } + }; diff --git a/social/email/locales/en-US/61-email.json b/social/email/locales/en-US/61-email.json index d825cb44..ae53b74c 100644 --- a/social/email/locales/en-US/61-email.json +++ b/social/email/locales/en-US/61-email.json @@ -45,6 +45,7 @@ "keyFile":"Private key", "users": "Users", "auth": "Authenticate users", + "noAuth": "No authentication", "addButton": "Add", "expert": "Expert" }, @@ -52,7 +53,8 @@ "tip": { "cred": "Note: Copied credentials from global emailkeys.js file.", "recent": "Tip: Only retrieves the single most recent email.", - "mta": "Note: To use ports below 1024 you may need elevated (root) privileges. See help sidebar." + "mta": "Note: To use ports below 1024 you may need elevated (root) privileges. See help sidebar.", + "mta-custom-auth": "Expected Format: [{\"name\":string, \"password\":string}, ...]" }, "status": { "messagesent": "Message sent: __response__", diff --git a/social/email/locales/ja/61-email.json b/social/email/locales/ja/61-email.json index 82c7bcc7..1cc49aa8 100644 --- a/social/email/locales/ja/61-email.json +++ b/social/email/locales/ja/61-email.json @@ -40,7 +40,8 @@ "tip": { "cred": "注釈: emailkeys.jsファイルから認証情報をコピーしました。", "recent": "注釈: 最新のメールを1件のみ取得します。", - "mta": "注釈: 1024未満のポートを使用するには、昇格された(root)特権が必要です。ヘルプサイドバーを参照してください。" + "mta": "注釈: 1024未満のポートを使用するには、昇格された(root)特権が必要です。ヘルプサイドバーを参照してください。", + "mta-custom-auth": "期待される形式: [{\"name\":string, \"password\":string}, ...]" }, "status": { "messagesent": "メッセージを送信しました: __response__",