Smtp server (#923)

* Email Mta Node added security and authentication

* Documentation updated

* Original formatting restored
This commit is contained in:
David D'Hauwe 2022-06-10 11:14:38 +02:00 committed by GitHub
parent dcfd055860
commit 0fa0816506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 248 additions and 51 deletions

View File

@ -306,7 +306,47 @@
<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 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>
<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>
@ -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,16 +566,38 @@ 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,
logger: false,
disabledCommands: ['AUTH', 'STARTTLS'],
onData: function (stream, session, callback) {
simpleParser(stream, { skipTextToHtml:true, skipTextLinks:true }, (err, parsed) => { simpleParser(stream, { skipTextToHtml:true, skipTextLinks:true }, (err, parsed) => {
if (err) { node.error(RED._("email.errors.parsefail"),err); } if (err) { node.error(RED._("email.errors.parsefail"),err); }
else { else {
@ -605,7 +628,19 @@ module.exports = function(RED) {
callback(); 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

@ -58,11 +58,19 @@
<script type="text/html" data-help-name="e-mail mta"> <script type="text/html" data-help-name="e-mail mta">
<p>Mail Transfer Agent - listens on a port for incoming SMTP mails.</p> <p>Mail Transfer Agent - listens on a port for incoming SMTP mails.</p>
<p><b>Note</b>: "NOT for production use" as there is no security built in. <p><b>Note</b>: Default configuration is "NOT for production use" as security is not enabled.
This is primarily for local testing of outbound mail sending, but could be used This is primarily for local testing of outbound mail sending, but could be used
as a mail forwarder to a real email service if required.</p> as a mail forwarder to a real email service if required.</p>
<p>To use ports below 1024, for example 25 or 465, you may need to get privileged access. <p>To use ports below 1024, for example 25 or 465, you may need to get privileged access.
On linux systems this can be done by running On linux systems this can be done by running
<pre>sudo setcap 'cap_net_bind_service=+eip' $(which node)</pre> <pre>sudo setcap 'cap_net_bind_service=+eip' $(which node)</pre>
and restarting Node-RED. Be aware - this gives all node applications access to all ports.</p> and restarting Node-RED. Be aware - this gives all node applications access to all ports.</p>
<h3>Security</h3>
<p>When <i>Secure connection</i> is checked, the connection will use TLS.
If not it is still possible to upgrade clear text socket to TLS socket by checking <i>Start TLS</i>.
If you do no specify your own certificate (path to file) then a pregenerated self-signed certificate is used. Any respectful client refuses to accept such certificate.</p>
<h3>Authentication</h3>
<p>Authentication can be enabled (PLAIN or LOGIN). Add at least one user.</p>
<h3>Expert</h3>
<p>All options as described in <a href="https://nodemailer.com/extras/smtp-server/" target="_new">nodemailer SMTP server</a> can be made here.</p>
</script> </script>

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": {