Email Node permit runtime users list for auth (#1104)

* Add i18n labels and tips

* improve name/pass fields size

* layout improvements

* typedInput for auth type
This commit is contained in:
Stephen McLaughlin
2025-08-26 09:59:56 +01:00
committed by GitHub
parent 4b1f12ae04
commit 93c2754784
4 changed files with 114 additions and 29 deletions

View File

@@ -417,7 +417,7 @@
<script type="text/html" data-template-name="e-mail mta">
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name" style="width: calc(100% - 110px)">
</div>
<div class="form-row">
<label for="node-input-port"><i class="fa fa-random"></i> <span data-i18n="email.label.port"></span></label>
@@ -443,12 +443,13 @@
</div>
<div class="form-row">
<label for="node-input-auth"><i class="fa fa-user"></i> <span data-i18n="email.label.users"></span></label>
<input type="checkbox" id="node-input-auth" style="display:inline-block; width:20px; vertical-align:baseline;">
<span data-i18n="email.label.auth"></span>
<input type="hidden" id="node-input-authType">
<input type="input" id="node-input-auth" style="width: calc(100% - 110px)">
</div>
<div class="form-tips" id="node-custom-auth-tip"><span data-i18n="[html]email.tip.mta-custom-auth"></span></div>
<div class="form-row node-input-email-users-container-row" style="margin-bottom: 0px;">
<div id="node-input-email-users-container-div" style="box-sizing: border-box; border-radius: 5px;
height: 200px; padding: 5px; border: 1px solid #ccc; overflow-y:scroll;">
min-height: 200px; padding: 5px; border: 1px solid #ccc; overflow-y:scroll; resize: vertical;">
<ol id="node-input-email-users-container" style="list-style-type:none; margin: 0;"></ol>
</div>
</div>
@@ -460,7 +461,7 @@
</div>
<div class="form-row">
<label for="node-input-expert"><i class="fa fa-cog"></i> <span data-i18n="email.label.expert"></span></label>
<input type="text" id="node-input-expert">
<input type="text" id="node-input-expert" style="width: calc(100% - 110px)">
</div>
<div class="form-tips" id="node-tip"><span data-i18n="[html]email.tip.mta"></span></div>
</script>
@@ -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;

View File

@@ -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)
}
})
})
}
};

View File

@@ -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": "<b>Note:</b> Copied credentials from global emailkeys.js file.",
"recent": "Tip: Only retrieves the single most recent email.",
"mta": "<b>Note:</b> To use ports below 1024 you may need elevated (root) privileges. See help sidebar."
"mta": "<b>Note:</b> To use ports below 1024 you may need elevated (root) privileges. See help sidebar.",
"mta-custom-auth": "<b>Expected Format:</b> <code>[{\"name\":string, \"password\":string}, ...]</code>"
},
"status": {
"messagesent": "Message sent: __response__",

View File

@@ -40,7 +40,8 @@
"tip": {
"cred": "<b>注釈:</b> emailkeys.jsファイルから認証情報をコピーしました。",
"recent": "注釈: 最新のメールを1件のみ取得します。",
"mta": "<b>注釈:</b> 1024未満のポートを使用するには、昇格された(root)特権が必要です。ヘルプサイドバーを参照してください。"
"mta": "<b>注釈:</b> 1024未満のポートを使用するには、昇格された(root)特権が必要です。ヘルプサイドバーを参照してください。",
"mta-custom-auth": "<b>期待される形式:</b> <code>[{\"name\":string, \"password\":string}, ...]</code>"
},
"status": {
"messagesent": "メッセージを送信しました: __response__",