mirror of
				https://github.com/node-red/node-red-nodes.git
				synced 2025-03-01 10:37:43 +00:00 
			
		
		
		
	Smtp server (#923)
* Email Mta Node added security and authentication * Documentation updated * Original formatting restored
This commit is contained in:
		@@ -306,8 +306,48 @@
 | 
			
		||||
<script type="text/html" data-template-name="e-mail mta">
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
        <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 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">
 | 
			
		||||
@@ -316,22 +356,127 @@
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script type="text/javascript">
 | 
			
		||||
    RED.nodes.registerType('e-mail mta',{
 | 
			
		||||
    RED.nodes.registerType('e-mail mta', {
 | 
			
		||||
        category: 'social',
 | 
			
		||||
        color:"#c7e9c0",
 | 
			
		||||
        color: "#c7e9c0",
 | 
			
		||||
        defaults: {
 | 
			
		||||
            name: {value:""},
 | 
			
		||||
            port: {value:"1025",required:true},
 | 
			
		||||
            name: { value: "" },
 | 
			
		||||
            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,
 | 
			
		||||
        outputs:1,
 | 
			
		||||
        inputs: 0,
 | 
			
		||||
        outputs: 1,
 | 
			
		||||
        icon: "envelope.png",
 | 
			
		||||
        paletteLabel: function() { return this._("email.email") + " MTA" },
 | 
			
		||||
        label: function() {
 | 
			
		||||
            return this.name||this._("email.email") + " MTA";
 | 
			
		||||
        paletteLabel: function () { return this._("email.email") + " MTA" },
 | 
			
		||||
        label: function () {
 | 
			
		||||
            return this.name || this._("email.email") + " MTA";
 | 
			
		||||
        },
 | 
			
		||||
        labelStyle: function() {
 | 
			
		||||
            return this.name?"node_label_italic":"";
 | 
			
		||||
        labelStyle: function () {
 | 
			
		||||
            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>
 | 
			
		||||
@@ -19,6 +19,7 @@ module.exports = function(RED) {
 | 
			
		||||
    var simpleParser = require("mailparser").simpleParser;
 | 
			
		||||
    var SMTPServer = require("smtp-server").SMTPServer;
 | 
			
		||||
    //var microMTA = require("micromta").microMTA;
 | 
			
		||||
    var fs = require('fs');
 | 
			
		||||
 | 
			
		||||
    if (parseInt(process.version.split("v")[1].split(".")[0]) < 8) {
 | 
			
		||||
        throw "Error : Requires nodejs version >= 8.";
 | 
			
		||||
@@ -565,47 +566,81 @@ module.exports = function(RED) {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    function EmailMtaNode(n) {
 | 
			
		||||
        RED.nodes.createNode(this,n);
 | 
			
		||||
        RED.nodes.createNode(this, n);
 | 
			
		||||
        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;
 | 
			
		||||
        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({
 | 
			
		||||
            secure: false,
 | 
			
		||||
            logger: false,
 | 
			
		||||
            disabledCommands: ['AUTH', 'STARTTLS'],
 | 
			
		||||
 | 
			
		||||
            onData: function (stream, session, callback) {
 | 
			
		||||
                simpleParser(stream, { skipTextToHtml:true, skipTextLinks:true }, (err, parsed) => {
 | 
			
		||||
                    if (err) { node.error(RED._("email.errors.parsefail"),err); }
 | 
			
		||||
                    else {
 | 
			
		||||
                        node.status({fill:"green", shape:"dot", text:""});
 | 
			
		||||
                        var msg = {}
 | 
			
		||||
                        msg.payload = parsed.text;
 | 
			
		||||
                        msg.topic = parsed.subject;
 | 
			
		||||
                        msg.date = parsed.date;
 | 
			
		||||
                        msg.header = {};
 | 
			
		||||
                        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);
 | 
			
		||||
        node.options.onData = function (stream, session, callback) {
 | 
			
		||||
            simpleParser(stream, { skipTextToHtml:true, skipTextLinks:true }, (err, parsed) => {
 | 
			
		||||
                if (err) { node.error(RED._("email.errors.parsefail"),err); }
 | 
			
		||||
                else {
 | 
			
		||||
                    node.status({fill:"green", shape:"dot", text:""});
 | 
			
		||||
                    var msg = {}
 | 
			
		||||
                    msg.payload = parsed.text;
 | 
			
		||||
                    msg.topic = parsed.subject;
 | 
			
		||||
                    msg.date = parsed.date;
 | 
			
		||||
                    msg.header = {};
 | 
			
		||||
                    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; }
 | 
			
		||||
                    }
 | 
			
		||||
                    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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -58,11 +58,19 @@
 | 
			
		||||
 | 
			
		||||
<script type="text/html" data-help-name="e-mail mta">
 | 
			
		||||
    <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
 | 
			
		||||
    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.
 | 
			
		||||
    On linux systems this can be done by running
 | 
			
		||||
    <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>
 | 
			
		||||
    <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>
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,16 @@
 | 
			
		||||
            "never": "never",
 | 
			
		||||
            "required": "if required",
 | 
			
		||||
            "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__",
 | 
			
		||||
        "tip": {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user