Viewing File: /usr/local/cpanel/base/sharedjs/email_deliverability/services/spfParser.js

/*
 * email_deliverability/services/spfParser.js         Copyright 2022 cPanel, L.L.C.
 *                                                           All rights reserved.
 * copyright@cpanel.net                                         http://cpanel.net
 * This code is subject to the cPanel license. Unauthorized copying is prohibited
 */


define(
    ["lodash"],
    function(_) {

        "use strict";

        var MechanismError = function MechanismError(message, type) {
            this.name = "MechanismError";
            this.message = message;
            this.type = type || "warning";
            this.stack = new Error().stack;
        };

        function domainPrefixCheck(name, pattern, term) {
            var parts = term.match(pattern);
            var value = parts[1];

            if (!value) {
                return null;
            }

            if (value === ":" || value === "/") {
                throw new MechanismError("Blank argument for the " + name + " mechanism", "error");
            }

            // Value starts with ":" so it"s a domain
            if (/^:/.test(value)) {
                value = value.replace(/^:/, "");
            }

            return value;
        }

        function domainCheckNullable(name, pattern, term) {
            return domainCheck(name, pattern, term, true);
        }

        function domainCheck(name, pattern, term, nullable) {
            var value = term.match(pattern)[1];

            if (!nullable && !value) {
                throw new MechanismError("Missing mandatory argument for the " + name + " mechanism", "error");
            }

            if (value === ":" || value === "=") {
                throw new MechanismError("Blank argument for the " + name + " mechanism", "error");
            }

            if (/^(:|=)/.test(value)) {
                value = value.replace(/^(:|=)/, "");
            }

            return value;
        }

        MechanismError.prototype = Object.create(Error.prototype);
        MechanismError.prototype.constructor = MechanismError;

        var MECHANISMS = {
            version: {
                description: "The SPF record version",
                pattern: /^v=(.+)$/i,
                validate: function validate(r) {
                    var version = r.match(this.pattern)[1]; // NOTE: This test can never work since we force match it to spf1 in index.js
                    // if (version !== 'spf1') {
                    // 	throw new MechanismError(`Invalid version '${version}', must be 'spf1'`);
                    // }

                    return version;
                },
            },
            all: {
                description: "Always matches. It goes at the end of your record",
                pattern: /^all$/i,
            },
            ip4: {

                // ip4:<ip4-address>
                // ip4:<ip4-network>/<prefix-length>
                description: "Match if IP is in the given range",
                pattern: /^ip4:(([\d.]*)(\/\d+)?)$/i,
                validate: function validate(r) {
                    var parts = r.match(this.pattern);
                    var value = parts[1];

                    if (!value) {
                        throw new MechanismError("Missing or blank mandatory network specification for the 'ip4' mechanism.", "error");
                    }

                    return value;
                },
            },
            ip6: {

                // ip6:<ip6-address>
                // ip6:<ip6-network>/<prefix-length>
                description: "Match if IPv6 is in the given range",
                pattern: /^ip6:((.*?)(\/\d+)?)$/i,
                validate: function validate(r) {
                    var parts = r.match(this.pattern);
                    var value = parts[1];

                    if (!value) {
                        throw new MechanismError("Missing or blank mandatory network specification for the 'ip6' mechanism.", "error");
                    }

                    return value;
                },
            },
            a: {

                // a
                // a/<prefix-length>
                // a:<domain>
                // a:<domain>/<prefix-length>
                description: "Match if IP has a DNS 'A' record in given domain",
                pattern: /a((:.*?)?(\/\d*)?)?$/i,
                validate: function validate(r) {
                    return domainPrefixCheck("a", this.pattern, r);
                },
            },
            mx: {

                // mx
                // mx/<prefix-length>
                // mx:<domain>
                // mx:<domain>/<prefix-length>
                description: "",
                pattern: /mx((:.*?)?(\/\d*)?)?$/i,
                validate: function validate(r) {
                    return domainPrefixCheck("mx", this.pattern, r);
                },
            },
            ptr: {

                // ptr
                // ptr:<domain>
                description: "Match if IP has a DNS 'PTR' record within given domain",
                pattern: /^ptr(:.*?)?$/i,
                validate: function validate(r) {
                    return domainCheckNullable("ptr", this.pattern, r);
                },
            },
            exists: {
                pattern: /^exists(:.*?)?$/i,
                validate: function validate(r) {
                    return domainCheck("exists", this.pattern, r);
                },
            },
            include: {
                description: "The specified domain is searched for an 'allow'",
                pattern: /^include(:.*?)?$/i,
                validate: function validate(r) {
                    return domainCheck("include", this.pattern, r);
                },
            },
            redirect: {
                description: "The SPF record for the value replaces the current record",
                pattern: /redirect(=.*?)?$/i,
                validate: function validate(r) {
                    return domainCheck("redirect", this.pattern, r);
                },
            },
            exp: {
                description: "Explanation message to send with rejection",
                pattern: /exp(=.*?)?$/i,
                validate: function validate(r) {
                    return domainCheck("exp", this.pattern, r);
                },
            },
        };

        var PREFIXES = {
            "+": "Pass",
            "-": "Fail",
            "~": "SoftFail",
            "?": "Neutral",
        };

        var versionRegex = /^v=spf1/i;
        var mechanismRegex = /(\+|-|~|\?)?(.+)/i; // * Values that will be set for every mechanism:
        // Prefix
        // Type
        // Value
        // PrefixDesc
        // Description

        function parseTerm(term, messages) {

            // Match up the prospective mechanism against the mechanism regex
            var parts = term.match(mechanismRegex);
            var record = {}; // It matched! Let's try to see which specific mechanism type it matches

            if (parts !== null) {

                // Break up the parts into their pieces
                var prefix = parts[1];
                var mechanism = parts[2]; // Check qualifier

                if (prefix) {
                    record.prefix = prefix;
                    record.prefixdesc = PREFIXES[prefix];
                } else if (versionRegex.test(mechanism)) {
                    record.prefix = "v";
                } else {

                    // Default to "pass" qualifier
                    record.prefix = "+";
                    record.prefixdesc = PREFIXES["+"];
                }

                var found = false;

                for (var name in MECHANISMS) {
                    if (Object.prototype.hasOwnProperty.call(MECHANISMS, name)) {
                        var settings = MECHANISMS[name]; // Matches mechanism spec

                        if (settings.pattern.test(mechanism)) {
                            found = true;
                            record.type = name;
                            record.description = settings.description;

                            if (settings.validate) {
                                try {
                                    var value = settings.validate.call(settings, mechanism);

                                    if (typeof value !== "undefined" && value !== null) {
                                        record.value = value;
                                    }
                                } catch (err) {
                                    if (err instanceof MechanismError) {

                                        // Error validating mechanism
                                        messages.push({
                                            message: err.message,
                                            type: err.type,
                                        });
                                        break;
                                    } // else {
                                    // 	throw err;
                                    // }

                                }
                            }

                            break;
                        }
                    }
                }

                if (!found) {
                    messages.push({
                        message: "Unknown standalone term '".concat(mechanism, "'"),
                        type: "error",
                    });
                }
            }


            return record;
        }

        function parse(record) {

            // Remove whitespace
            record = record.trim();
            var records = {
                mechanisms: [],
                messages: [],

                // Valid flag will be changed at end of function
                valid: false,
            };

            if (!versionRegex.test(record)) {

                // throw new Error();
                records.messages.push({
                    message: "No valid version found, record must start with 'v=spf1'",
                    type: "error",
                });
                return records;
            }

            var terms = record.split(/\s+/); // Give an error for duplicate Modifiers

            var duplicateMods = terms.filter(function(x) {
                return new RegExp("=").test(x);
            }).map(function(x) {
                return x.match(/^(.*?)=/)[1];
            }).filter(function(x, i, arr) {
                return _.includes(arr, x, i + 1);
            });

            if (duplicateMods && duplicateMods.length > 0) {
                records.messages.push({
                    type: "error",
                    message: "Modifiers like \"".concat(duplicateMods[0], "\" may appear only once in an SPF string"),
                });
                return records;
            } // Give warning for duplicate mechanisms


            var duplicateMechs = terms.map(function(x) {
                return x.replace(/^(\+|-|~|\?)/, "");
            }).filter(function(x, i, arr) {
                return _.includes(arr, x, i + 1);
            });

            if (duplicateMechs && duplicateMechs.length > 0) {
                records.messages.push({
                    type: "warning",
                    message: "One or more duplicate mechanisms were found in the policy",
                });
            }


            try {
                for (var i = 0; i < terms.length; i++) {
                    var term = terms[i];
                    var mechanism = parseTerm(term, records.messages);

                    if (mechanism) {
                        records.mechanisms.push(mechanism);
                    }
                } // See if there's an "all" or "redirect" at the end of the policy
            } catch (err) {
            // eslint-disable-next-line no-console
                console.error(err);
            }

            if (records.mechanisms.length > 0) {

                // More than one modifier like redirect or exp is invalid
                // if (records.mechanisms.filter(x => x.type === 'redirect').length > 1 || records.mechanisms.filter(x => x.type === 'exp').length > 1) {
                // 	records.messages.push({
                // 		type: 'error',
                // 		message: 'Modifiers like "redirect" and "exp" can only appear once in an SPF string'
                // 	});
                // 	return records;
                // }
                // let lastMech = records.mechanisms[records.mechanisms.length - 1];
                var redirectMech = _.find(records.mechanisms, function(x) {
                    return x.type === "redirect";
                });
                var allMech = _.find(records.mechanisms, function(x) {
                    return x.type === "all";
                }); // if (lastMech.type !== "all" && lastMech !== "redirect") {

                if (!allMech && !redirectMech) {
                    records.messages.push({
                        type: "warning",
                        message: 'SPF strings should always either use an "all" mechanism or a "redirect" modifier to explicitly terminate processing.',
                    });
                } // Give a warning if "all" is not last mechanism in policy


                var allIdx = -1;

                records.mechanisms.forEach(function(x, index) {
                    if (x.type === "all" && allIdx === -1) {
                        allIdx = index;
                    }
                });

                if (allIdx > -1) {
                    if (allIdx < records.mechanisms.length - 1) {
                        records.messages.push({
                            type: "warning",
                            message: "One or more mechanisms were found after the \"all\" mechanism. These mechanisms will be ignored",
                        });
                    }
                } // Give a warning if there"s a redirect modifier AND an "all" mechanism


                if (redirectMech && allMech) {
                    records.messages.push({
                        type: "warning",
                        message: 'The "redirect" modifier will not be used, because the SPF string contains an "all" mechanism. A "redirect" modifier is only used after all mechanisms fail to match, but "all" will always match',
                    });
                }
            } // If there are no messages, delete the key from "records"


            if (!Object.keys(records.messages).length > 0) {
                delete records.messages;
            }

            records.valid = true;
            return records;
        }

        return {
            parse: parse,
            parseTerm: parseTerm,
            mechanisms: MECHANISMS,
            prefixes: PREFIXES,
        };
    }
);
Back to Directory File Manager