Email Mta Node added security and authentication

This commit is contained in:
David D'Hauwe 2022-06-06 21:08:17 +02:00
parent 7db260dcc3
commit 6fcb844cb6
3 changed files with 356 additions and 167 deletions

View File

@ -61,45 +61,45 @@
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
(function() { (function () {
RED.nodes.registerType('e-mail',{ RED.nodes.registerType('e-mail', {
category: 'social-output', category: 'social-output',
color:"#c7e9c0", color: "#c7e9c0",
defaults: { defaults: {
server: {value:"smtp.gmail.com",required:true}, server: { value: "smtp.gmail.com", required: true },
port: {value:"465",required:true}, port: { value: "465", required: true },
secure: {value: true}, secure: { value: true },
tls: {value: true}, tls: { value: true },
name: {value:""}, name: { value: "" },
dname: {value:""} dname: { value: "" }
}, },
credentials: { credentials: {
userid: {type:"text"}, userid: { type: "text" },
password: {type: "password"}, password: { type: "password" },
global: { type:"boolean"} global: { type: "boolean" }
}, },
inputs:1, inputs: 1,
outputs:0, outputs: 0,
icon: "envelope.png", icon: "envelope.png",
align: "right", align: "right",
paletteLabel: function() { paletteLabel: function () {
return this._("email.email"); return this._("email.email");
}, },
label: function() { label: function () {
return this.dname||this.name||this._("email.email"); return this.dname || this.name || this._("email.email");
}, },
labelStyle: function() { labelStyle: function () {
return (this.dname)?"node_label_italic":""; return (this.dname) ? "node_label_italic" : "";
}, },
oneditprepare: function() { oneditprepare: function () {
if (this.credentials.global) { if (this.credentials.global) {
$('#node-tip').show(); $('#node-tip').show();
} else { } else {
$('#node-tip').hide(); $('#node-tip').hide();
}
} }
} });
}); })();
})();
</script> </script>
@ -190,24 +190,24 @@
<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">
</div> </div>
<script> <script>
var checkPorts = function() { var checkPorts = function () {
var currentPort = $("#node-input-port").val(); var currentPort = $("#node-input-port").val();
if (currentPort === "143" || currentPort === "993" || currentPort === "110" || currentPort == "995") { if (currentPort === "143" || currentPort === "993" || currentPort === "110" || currentPort == "995") {
if ($("#node-input-useSSL").prop("checked") === true) { if ($("#node-input-useSSL").prop("checked") === true) {
$("#node-input-port").val($("#node-input-protocol").val() === "IMAP"?"993":"995"); $("#node-input-port").val($("#node-input-protocol").val() === "IMAP" ? "993" : "995");
} else { } else {
$("#node-input-port").val($("#node-input-protocol").val() === "IMAP"?"143":"110"); $("#node-input-port").val($("#node-input-protocol").val() === "IMAP" ? "143" : "110");
} }
} }
}; };
$("#node-input-useSSL").change(function(x, y) { $("#node-input-useSSL").change(function (x, y) {
// console.log("useSSL: x="+ JSON.stringify(x) + ", y=" + y); // console.log("useSSL: x="+ JSON.stringify(x) + ", y=" + y);
// console.log("Value: " + $("#node-input-useSSL").prop("checked")); // console.log("Value: " + $("#node-input-useSSL").prop("checked"));
checkPorts(); checkPorts();
}); });
$("#node-input-protocol").change(function() { $("#node-input-protocol").change(function () {
var protocol = $("#node-input-protocol").val(); var protocol = $("#node-input-protocol").val();
if (protocol === "IMAP") { if (protocol === "IMAP") {
$(".node-input-autotls").show(); $(".node-input-autotls").show();
@ -226,88 +226,128 @@
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
(function() { (function () {
RED.nodes.registerType('e-mail in',{ RED.nodes.registerType('e-mail in', {
category: 'social-input', category: 'social-input',
color:"#c7e9c0", color: "#c7e9c0",
defaults: { defaults: {
name: {value:""}, name: { value: "" },
protocol: {value: "IMAP", required:true}, // Which protocol to use to connect to the mail server ("IMAP" or "POP3") protocol: { value: "IMAP", required: true }, // Which protocol to use to connect to the mail server ("IMAP" or "POP3")
server: {value:"imap.gmail.com",required:true}, server: { value: "imap.gmail.com", required: true },
useSSL: {value: true}, useSSL: { value: true },
autotls: {value: "never"}, autotls: { value: "never" },
port: {value:"993",required:true}, port: { value: "993", required: true },
box: {value:"INBOX"}, // For IMAP, The mailbox to process box: { value: "INBOX" }, // For IMAP, The mailbox to process
disposition: { value: "Read" }, // For IMAP, the disposition of the read email disposition: { value: "Read" }, // For IMAP, the disposition of the read email
criteria: {value: "UNSEEN"}, criteria: { value: "UNSEEN" },
repeat: {value:"300",required:true}, repeat: { value: "300", required: true },
fetch: {value:"auto"}, fetch: { value: "auto" },
inputs: {value:0} inputs: { value: 0 }
}, },
credentials: { credentials: {
userid: {type:"text"}, userid: { type: "text" },
password: {type: "password"}, password: { type: "password" },
global: { type:"boolean"} global: { type: "boolean" }
}, },
inputs: 0, inputs: 0,
outputs: 1, outputs: 1,
icon: "envelope.png", icon: "envelope.png",
paletteLabel: function() { paletteLabel: function () {
return this._("email.email"); return this._("email.email");
}, },
label: function() { label: function () {
return this.name||this._("email.email"); return this.name || this._("email.email");
}, },
labelStyle: function() { labelStyle: function () {
return (this.name)?"node_label_italic":""; return (this.name) ? "node_label_italic" : "";
}, },
oneditprepare: function() { oneditprepare: function () {
var that = this; var that = this;
if (this.credentials.global) { if (this.credentials.global) {
$('#node-tip').show(); $('#node-tip').show();
} else { } else {
$('#node-tip').hide(); $('#node-tip').hide();
}
if (typeof this.box === 'undefined') {
$("#node-input-box").val("INBOX");
this.box = "INBOX";
}
if (typeof this.criteria === 'undefined') {
$("#node-input-criteria").val("UNSEEN");
this.criteria = "UNSEEN";
}
if (typeof this.autotls === 'undefined') {
$("#node-input-autotls").val("never");
this.autotls = "never";
}
if ($("#node-input-fetch").val() === null) { $("#node-input-fetch").val("auto"); }
$("#node-input-fetch").change(function() {
if ($("#node-input-fetch").val() === "trigger") {
$('#node-repeatTime').hide();
that.inputs = 1;
} }
else { if (typeof this.box === 'undefined') {
$('#node-repeatTime').show(); $("#node-input-box").val("INBOX");
that.inputs = 0; this.box = "INBOX";
} }
}); if (typeof this.criteria === 'undefined') {
$("#node-input-criteria").change(function() { $("#node-input-criteria").val("UNSEEN");
if ($("#node-input-criteria").val() === "_msg_") { this.criteria = "UNSEEN";
$("#node-input-fetch").val("trigger");
$("#node-input-fetch").change();
} }
}); if (typeof this.autotls === 'undefined') {
} $("#node-input-autotls").val("never");
}); this.autotls = "never";
})(); }
if ($("#node-input-fetch").val() === null) { $("#node-input-fetch").val("auto"); }
$("#node-input-fetch").change(function () {
if ($("#node-input-fetch").val() === "trigger") {
$('#node-repeatTime').hide();
that.inputs = 1;
}
else {
$('#node-repeatTime').show();
that.inputs = 0;
}
});
$("#node-input-criteria").change(function () {
if ($("#node-input-criteria").val() === "_msg_") {
$("#node-input-fetch").val("trigger");
$("#node-input-fetch").change();
}
});
}
});
})();
</script> </script>
<script type="text/html" data-template-name="e-mail mta"> <script type="text/html" data-template-name="e-mail mta">
<div class="form-row"> <div class="form-row">
<label for="node-input-port"><i class="fa fa-random"></i> <span data-i18n="email.label.port"></span></label> <label for="node-input-port"><i class="fa fa-random"></i> <span data-i18n="email.label.port"></span></label>
<input type="text" id="node-input-port" style="width:70%;"/> <input type="text" id="node-input-port" style="width:100px">
<label style="width:40px"> </label>
<input type="checkbox" id="node-input-secure" style="display:inline-block; width:20px; vertical-align:baseline;">
<span data-i18n="email.label.enableSecure"></span>
</div>
<div class="form-row">
<label for="node-input-starttls"><i class="fa fa-lock"></i> <span data-i18n="email.label.enableStarttls"></span></label>
<input type="checkbox" id="node-input-starttls" style="display:inline-block; width:20px; vertical-align:baseline;">
<span data-i18n="email.label.starttlsUpgrade"></span>
</div>
<div class="form-row">
<label for="node-input-certFile"><i class="fa fa-file"></i>
<span data-i18n="email.label.certFile"></span></label>
<input type="text" id="node-input-certFile" placeholder="server.crt" style="width:100%">
</div>
<div class="form-row">
<label for="node-input-keyFile"><i class="fa fa-key"></i>
<span data-i18n="email.label.keyFile"></span></label>
<input type="text" id="node-input-keyFile" placeholder="private.key" style="width:100%">
</div>
<div class="form-row">
<label for="node-input-auth"><i class="fa fa-user"></i> <span data-i18n="email.label.users"></span></label>
<label style="width:144px"> </label>
<input type="checkbox" id="node-input-auth" style="display:inline-block; width:20px; vertical-align:baseline;">
<span data-i18n="email.label.auth"></span>
</div> </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;">
<ol id="node-input-email-users-container" style="list-style-type:none; margin: 0;"></ol>
</div>
</div>
<div class="form-row">
<a href="#" class="editor-button editor-button-small" id="node-input-email-users-add" style="margin-top: 4px;">
<i class="fa fa-plus"></i>
<span data-i18n="email.label.addButton"></span>
</a>
</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">
</div>
<div class="form-row"> <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> <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">
@ -316,22 +356,127 @@
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
RED.nodes.registerType('e-mail mta',{ RED.nodes.registerType('e-mail mta', {
category: 'social', category: 'social',
color:"#c7e9c0", color: "#c7e9c0",
defaults: { defaults: {
name: {value:""}, name: { value: "" },
port: {value:"1025",required:true}, port: { value: "1025", required: true, validate: RED.validators.number() },
secure: { value: false },
starttls: { value: false },
certFile: { value: "" },
keyFile: { value: "" },
users: { value: [] },
auth: { value: false },
expert: { value: '{"logger":false}' }
}, },
inputs:0, inputs: 0,
outputs:1, outputs: 1,
icon: "envelope.png", icon: "envelope.png",
paletteLabel: function() { return this._("email.email") + " MTA" }, paletteLabel: function () { return this._("email.email") + " MTA" },
label: function() { label: function () {
return this.name||this._("email.email") + " MTA"; return this.name || this._("email.email") + " MTA";
}, },
labelStyle: function() { labelStyle: function () {
return this.name?"node_label_italic":""; return this.name ? "node_label_italic" : "";
},
oneditprepare: function () {
let node = this;
// Expert settings
$("#node-input-expert").typedInput({
type: "json",
types: ["json"]
})
// User Management
let cacheItemCount = 0;
if (node.users && node.users.length > 0) {
cacheItemCount = node.users.length;
node.users.forEach(function (element, index, array) {
generateUserEntry(element, index);
});
}
function generateUserEntry(user, id) {
let container = $("<li/>", {
style: "background: #fefefe; margin:0; padding:8px 0px; border-bottom: 1px solid #ccc;"
});
let row = $('<div id="row' + id + '"/>').appendTo(container);
$('<i style="color: #eee; cursor: move;" class="node-input-email-users-handle fa fa-bars"></i>').appendTo(row);
let userField = $("<input/>", {
id: "node-input-email-users-name" + id,
class: "userName",
type: "text",
style: "margin-left:5px;width:100px;",
placeholder: "name"
}).appendTo(row);
let passwordField = $("<input/>", {
id: "node-input-email-users-password" + id,
class: "userPassword",
type: "password",
style: "margin: 0 auto;width:50%;min-width:20px;margin-left:5px",
placeholder: "password"
}).appendTo(row);
userField.val(user.name);
passwordField.val(user.password);
let finalspan = $("<span/>", {
style: "float: right;margin-right: 10px;"
}).appendTo(row);
let removeUserButton = $("<a/>", {
href: "#",
id: "node-button-user-remove" + id,
class: "editor-button editor-button-small",
style: "margin-top: 7px; margin-left: 5px;"
}).appendTo(finalspan);
$("<i/>", { class: "fa fa-remove" }).appendTo(removeUserButton);
removeUserButton.click(function () {
container.css({ background: "#fee" });
container.fadeOut(300, function () {
$(this).remove();
});
});
$("#node-input-email-users-container").append(container);
}
$("#node-input-email-users-container").sortable({
axis: "y",
handle: ".node-input-email-users-handle",
cursor: "move"
});
$("#node-input-email-users-container .node-input-email-users-handle").disableSelection();
$("#node-input-email-users-add").click(function () {
if (!cacheItemCount || cacheItemCount < 0) {
cacheItemCount = 0;
}
generateUserEntry({ name: "", password: "" }, cacheItemCount++);
$("#node-input-email-users-container-div").scrollTop(
$("#node-input-email-users-container-div").get(0).scrollHeight
);
});
},
oneditsave: function () {
let node = this;
let cacheUsers = $("#node-input-email-users-container").children();
node.users = [];
cacheUsers.each(function () {
node.users.push({
name: $(this)
.find(".userName")
.val(),
password: $(this)
.find(".userPassword")
.val()
});
});
} }
}); });
</script> </script>

View File

@ -19,6 +19,7 @@ module.exports = function(RED) {
var simpleParser = require("mailparser").simpleParser; var simpleParser = require("mailparser").simpleParser;
var SMTPServer = require("smtp-server").SMTPServer; var SMTPServer = require("smtp-server").SMTPServer;
//var microMTA = require("micromta").microMTA; //var microMTA = require("micromta").microMTA;
var fs = require('fs');
if (parseInt(process.version.split("v")[1].split(".")[0]) < 8) { if (parseInt(process.version.split("v")[1].split(".")[0]) < 8) {
throw "Error : Requires nodejs version >= 8."; throw "Error : Requires nodejs version >= 8.";
@ -565,47 +566,81 @@ module.exports = function(RED) {
function EmailMtaNode(n) { function EmailMtaNode(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this, n);
this.port = n.port; this.port = n.port;
this.secure = n.secure;
this.starttls = n.starttls;
this.certFile = n.certFile;
this.keyFile = n.keyFile;
this.users = n.users;
this.auth = n.auth;
try {
this.options = JSON.parse(n.expert);
} catch (error) {
this.options = {};
}
var node = this; var node = this;
if (!Array.isArray(node.options.disabledCommands)) {
node.options.disabledCommands = [];
}
node.options.secure = node.secure;
if (node.certFile) {
node.options.cert = fs.readFileSync(node.certFile);
}
if (node.keyFile) {
node.options.key = fs.readFileSync(node.keyFile);
}
if (!node.starttls) {
node.options.disabledCommands.push("STARTTLS");
}
if (!node.auth) {
node.options.disabledCommands.push("AUTH");
}
node.mta = new SMTPServer({ node.options.onData = function (stream, session, callback) {
secure: false, simpleParser(stream, { skipTextToHtml:true, skipTextLinks:true }, (err, parsed) => {
logger: false, if (err) { node.error(RED._("email.errors.parsefail"),err); }
disabledCommands: ['AUTH', 'STARTTLS'], else {
node.status({fill:"green", shape:"dot", text:""});
onData: function (stream, session, callback) { var msg = {}
simpleParser(stream, { skipTextToHtml:true, skipTextLinks:true }, (err, parsed) => { msg.payload = parsed.text;
if (err) { node.error(RED._("email.errors.parsefail"),err); } msg.topic = parsed.subject;
else { msg.date = parsed.date;
node.status({fill:"green", shape:"dot", text:""}); msg.header = {};
var msg = {} parsed.headers.forEach((v, k) => {msg.header[k] = v;});
msg.payload = parsed.text; if (parsed.html) { msg.html = parsed.html; }
msg.topic = parsed.subject; if (parsed.to) {
msg.date = parsed.date; if (typeof(parsed.to) === "string" && parsed.to.length > 0) { msg.to = parsed.to; }
msg.header = {}; else if (parsed.to.hasOwnProperty("text") && parsed.to.text.length > 0) { msg.to = parsed.to.text; }
parsed.headers.forEach((v, k) => {msg.header[k] = v;});
if (parsed.html) { msg.html = parsed.html; }
if (parsed.to) {
if (typeof(parsed.to) === "string" && parsed.to.length > 0) { msg.to = parsed.to; }
else if (parsed.to.hasOwnProperty("text") && parsed.to.text.length > 0) { msg.to = parsed.to.text; }
}
if (parsed.cc) {
if (typeof(parsed.cc) === "string" && parsed.cc.length > 0) { msg.cc = parsed.cc; }
else if (parsed.cc.hasOwnProperty("text") && parsed.cc.text.length > 0) { msg.cc = parsed.cc.text; }
}
if (parsed.cc && parsed.cc.length > 0) { msg.cc = parsed.cc; }
if (parsed.bcc && parsed.bcc.length > 0) { msg.bcc = parsed.bcc; }
if (parsed.from && parsed.from.value && parsed.from.value.length > 0) { msg.from = parsed.from.value[0].address; }
if (parsed.attachments) { msg.attachments = parsed.attachments; }
else { msg.attachments = []; }
node.send(msg); // Propagate the message down the flow
setTimeout(function() { node.status({})}, 500);
} }
callback(); if (parsed.cc) {
}); if (typeof(parsed.cc) === "string" && parsed.cc.length > 0) { msg.cc = parsed.cc; }
else if (parsed.cc.hasOwnProperty("text") && parsed.cc.text.length > 0) { msg.cc = parsed.cc.text; }
}
if (parsed.cc && parsed.cc.length > 0) { msg.cc = parsed.cc; }
if (parsed.bcc && parsed.bcc.length > 0) { msg.bcc = parsed.bcc; }
if (parsed.from && parsed.from.value && parsed.from.value.length > 0) { msg.from = parsed.from.value[0].address; }
if (parsed.attachments) { msg.attachments = parsed.attachments; }
else { msg.attachments = []; }
node.send(msg); // Propagate the message down the flow
setTimeout(function() { node.status({})}, 500);
}
callback();
});
}
node.options.onAuth = function (auth, session, callback) {
let id = node.users.findIndex(function (item) {
return item.name === auth.username;
});
if (id >= 0 && node.users[id].password === auth.password) {
callback(null, { user: id + 1 });
} else {
callback(new Error("Invalid username or password"));
} }
}); }
node.mta = new SMTPServer(node.options);
node.mta.listen(node.port); node.mta.listen(node.port);

View File

@ -34,7 +34,16 @@
"never": "never", "never": "never",
"required": "if required", "required": "if required",
"always": "always", "always": "always",
"rejectUnauthorised": "Check server certificate is valid" "rejectUnauthorised": "Check server certificate is valid",
"enableSecure": "Secure connection",
"enableStarttls": "Start TLS",
"starttlsUpgrade": "Upgrade cleartext connection with STARTTLS",
"certFile": "Certificate",
"keyFile":"Private key",
"users": "Users",
"auth": "Authenticate users",
"addButton": "Add",
"expert": "Expert"
}, },
"default-message": "__description__\n\nFile from Node-RED is attached: __filename__", "default-message": "__description__\n\nFile from Node-RED is attached: __filename__",
"tip": { "tip": {