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

//                                      Copyright 2024 WebPros International, LLC
//                                                           All rights reserved.
// copyright@cpanel.net                                         http://cpanel.net
// This code is subject to the cPanel license. Unauthorized copying is prohibited.

define(
    [
        "angular",
        "lodash",
        "cjt/util/locale",
        "shared/js/email_deliverability/services/domainFactory",
        "shared/js/email_deliverability/services/DKIMRecordProcessor",
        "shared/js/email_deliverability/services/DMARCRecordProcessor",
        "shared/js/email_deliverability/services/SPFRecordProcessor",
        "shared/js/email_deliverability/services/PTRRecordProcessor",
        "cjt/modules",
        "cjt/services/alertService",
        "cjt/modules",
        "cjt/services/APICatcher",
    ],
    function(angular, _, LOCALE, domainFactory, DKIMRecordProcessor, DMARCRecordProcessor, SPFRecordProcessor, PTRRecordProcessor) {

        "use strict";

        var MODULE_NAMESPACE = "shared.emailDeliverability.services.domains";
        var SERVICE_NAME = "DomainsService";
        var MODULE_REQUIREMENTS = [ "cjt2.services.apicatcher", "cjt2.services.alert" ];
        var SERVICE_INJECTABLES = ["$q", "$log", "$interval", "alertService", "APIInitializer", "APICatcher", "DOMAIN_TYPE_CONSTANTS", "PAGE"];

        // UAPI’s EmailAuth::validate_current_dkims returns the DKIM
        // record name as “domain”. This function intends to strip
        // the subdomain from that name to arrive at the domain name
        // for which the record exists. For example,
        // “newyork._domainkey.bob.com” becomes just “bob.com”.
        function _STRIP_DKIM_SUBDOMAIN(d) {
            return d.replace(/^.+\._domainkey\./, "");
        }

        var Zone = function(zoneName) {
            this.zone = zoneName;
            this.domains = [];
            this.lockDomain = null;
        };

        Zone.prototype.addDomain = function addDomainToZone(domain) {
            this.domains.push(domain);
        };

        // Returns a Domain object or null.
        Zone.prototype.getLockDomain = function getLockDomain() {
            return this.lockDomain;
        };

        // Requires a Domain object.
        Zone.prototype.lock = function lockZone(domainObj) {
            if (typeof domainObj !== "object") {
                throw new Error("Give a domain object!");
            }

            if (this.lockDomain) {
                throw new Error("Zone " + this.zone + " cannot lock for " + domainObj.domain + " because the zone is already locked for " + this.lockDomain + "!");
            }

            this.lockDomain = domainObj;
        };

        Zone.prototype.unlock = function unlockZone() {
            this.lockDomain = null;
        };


        /**
         *
         * Service Factory to generate the Domains service
         *
         * @module DomainsService
         * @memberof cpanel.emailDeliverability
         *
         * @param {ng.$q} $q
         * @param {ng.$log} $log
         * @param {Object} APICatcher base service
         * @param {Object} DOMAIN_TYPE_CONSTANTS constants lookup of domain types
         * @param {Object} PAGE window PAGE object created in template
         * @returns {Domains} instance of the Domains service
         */
        var SERVICE_FACTORY = function($q, $log, $interval, $alertService, $apiInit, APICatcher, DOMAIN_TYPE_CONSTANTS, PAGE) {

            var _flattenedDomains;
            var _domainLookupMap = {};
            var _domainZoneMap = {};
            var _zones = [];

            var Domains = function() {};

            Domains.prototype = Object.create(APICatcher);

            /**
             *
             * Cache domains for ease of later lookup
             *
             * @private
             *
             * @param {Domain} domain to cache for later lookup
             * @returns {Domain} stored domain
             */
            Domains.prototype._cacheDomain = function _cacheDomain(domain) {

                if (!_flattenedDomains) {
                    _flattenedDomains = [];
                }

                _domainLookupMap[domain.domain] = domain;

                // Cache DKIM Key Domain too
                _domainLookupMap["default._domainkey." + domain.domain] = domain;

                // Store Domain in flattened domains
                _flattenedDomains.push(domain);

                return domain;
            };

            /**
             *
             * Remove a domain from the cache lookup
             *
             * @private
             *
             * @param {Domain} domain to remove from cache
             * @returns {Boolean} success of domain removal
             */
            Domains.prototype._uncacheDomain = function _uncacheDomain(domain) {
                var self = this;

                if (!_flattenedDomains) {
                    return false;
                }

                var domainObject = self._getDomainObject(domain);

                for (var i = _flattenedDomains.length - 1; i >= 0; i--) {
                    if (_flattenedDomains[i].domain === domainObject.domain) {
                        _flattenedDomains.splice(i, 1);
                        return true;
                    }
                }

                return false;
            };

            /**
             *
             * Return a domain object. This is used to ensure you're always working with a Domain()
             * even when a string might have been passed to a function.
             *
             * @public
             *
             * @param {string|Domain} wantedDomain - domain name or domain object
             * @returns {Domain} matching wantedDomain
             */
            Domains.prototype._getDomainObject = function _getDomainObject(wantedDomain) {

                var self = this;

                var domain = wantedDomain;

                if (typeof domain === "string") {
                    domain = self.findDomainByName(domain);
                }

                if (domain instanceof domainFactory.getClass() !== true) {
                    $log.warn("Could not find domain “" + wantedDomain + "”");
                }

                return domain;
            };

            /**
             *
             * Find a domain using the domain name string
             *
             * @public
             *
             * @param {string} domainName - domain name to find
             * @returns {Domain} matching domainName
             */
            Domains.prototype.findDomainByName = function _findDomainByName(domainName) {
                return _domainLookupMap[domainName];
            };

            /**
             *
             * Returns all currently loaded and stored domains
             *
             * @public
             *
             * @returns Array<Domain> returns a list of all loaded domains
             */
            Domains.prototype.getAll = function getAllDomains() {
                return _flattenedDomains;
            };

            /**
             *
             * API Wrapper to fetch the all domains (main, addon, subdomain, alias) and cache them
             *
             * @public
             *
             * @returns Promise<Array<Domain>>
             */
            Domains.prototype.fetchAll = function fetchAllDomains() {
                var self = this;

                if (self.getAll()) {
                    return $q.resolve(self.getAll());
                }

                _flattenedDomains = [];

                var domains = PAGE.domains || [];

                domains.forEach(function(domain) {
                    if (domain.substring(0, 2) === "*.") {
                        return;
                    }
                    var domainObj = domainFactory.create({
                        domain: domain,
                        type: domain === PAGE.mainDomain ? DOMAIN_TYPE_CONSTANTS.MAIN : DOMAIN_TYPE_CONSTANTS.DOMAIN,
                    });
                    self._cacheDomain(domainObj);
                });

                return $q.resolve(self.getAll());

            };

            /**
             *
             * Extract Mail IPS from a validate_current_ptrs API Result
             *
             * @private
             *
             * @param {Object} results object from API with .data array
             */
            Domains.prototype._extractMailIPs = function _extractMailIPs(results) {
                var data = results.data;

                data.forEach(function(ptrResult) {
                    var domain = this._getDomainObject(ptrResult.domain);
                    if (domain) {
                        domain.setMailIP(ptrResult.ip_version, ptrResult.ip_address);
                    }
                }, this);

            };

            /**
             * Find a domain zone by the domain name
             *
             * @param {String} zoneName name of the zone to find
             * @returns {Zone}
             */
            Domains.prototype.findZoneByName = function findZoneByName(zoneName) {
                for (var i = 0; i < _zones.length; i++) {
                    if (_zones[i].zone === zoneName) {
                        return _zones[i];
                    }
                }

                return null;
            };

            /**
             * Find domain zone by domain or domain name
             *
             * @param {Domain|String} domain
             * @returns {Zone}
             */
            Domains.prototype.findZoneByDomain = function findZoneByDomain(domain) {
                var domainObj = this._getDomainObject(domain);
                return _domainLookupMap[domainObj.domain];
            };


            /**
             * Add a domain to an existing zone, or create it
             *
             * @private
             *
             * @param {Domain} domain
             * @param {String} zone
             */
            Domains.prototype._addDomainToZone = function _addDomainToZone(domain, zone) {
                var zoneObj = this.findZoneByName(zone);
                if (!zoneObj) {
                    zoneObj = new Zone(zone);
                    _zones.push(zoneObj);
                }
                domain.zone = zone;
                zoneObj.addDomain(domain);
                _domainZoneMap[domain.domain] = zoneObj;
            };

            /**
             *
             * Process the results of a has_ns_authority results item
             *
             * @private
             *
             * @param {Object} resultItem
             * @returns {Domain} the updated Domain
             */
            Domains.prototype._processNSAuthResultItem = function _processNSAuthResultItem(resultItem) {
                var domainObj = this._getDomainObject(resultItem.domain);
                if (domainObj) {
                    domainObj.soaError = resultItem.error || undefined;
                    domainObj.hasNSAuthority = resultItem.local_authority.toString() === "1";
                    domainObj.nameservers = resultItem.nameservers;
                    this._addDomainToZone(domainObj, resultItem.zone);
                }
                return domainObj;
            };

            /**
             *
             * Process a results item based on recordType and add records and suggested records to domains
             *
             * @private
             *
             * @param {Object} processor for handling recordType example services/DKIMRecordProcessor
             * @param {Object} recordTypeResults api results for the specific api call {data:...}
             * @param {string} recordType record type being processed
             */
            Domains.prototype._processRecordTypeResults = function _processRecordTypeResults(processor, recordTypeResults, recordType) {

                var normalizedResults = processor.normalizeResults(recordTypeResults);

                if (recordType === "ptr") {
                    this._extractMailIPs(recordTypeResults);
                }

                var normalizedResultsData = normalizedResults.data;
                var processedResults = processor.processResultItems(normalizedResultsData);
                normalizedResultsData.forEach(function(normalizedResultItem, index) {
                    var domainObj = this._getDomainObject(normalizedResultItem.domain);
                    if (!domainObj) {
                        $log.debug("domain not found", normalizedResultItem);
                        return;
                    }
                    var domainRecords = processedResults[index];
                    var suggestedRecord = processor.generateSuggestedRecord(normalizedResultItem);
                    domainObj.setSuggestedRecord(recordType, suggestedRecord);

                    domainObj.setRecordDetails(recordType, normalizedResultItem.details || normalizedResultItem);

                    if (recordType === "spf") {
                        domainObj.setExpectedMatch(recordType, normalizedResultItem.expected);
                    }

                    domainRecords.forEach( domainObj.addRecord.bind(domainObj) );
                }, this);

            };

            function _reportDKIMValidityCacheUpdates(recordTypeResults) {
                var validityCacheUpdated = recordTypeResults.data.filter( function(item) {
                    return item.validity_cache_update === "set";
                } );

                if (validityCacheUpdated.length) {
                    var domains = validityCacheUpdated.map( function(item) {
                        return _.escape( _STRIP_DKIM_SUBDOMAIN(item.domain) );
                    } );

                    domains = _.sortBy( domains, function(d) {
                        return d.length;
                    } );

                    // “domains” shouldn’t be a long list. If it becomes
                    // so, we’ll need to chop this up so that it gives just
                    // a few domains and then says something like
                    // “… and N others”. That will avoid creating a big blob
                    // in what should normally be just an informative alert.
                    $alertService.add({
                        type: "info",
                        replace: false,
                        message: LOCALE.maketext("The system detected [quant,_1,domain,domains] whose [output,acronym,DKIM,DomainKeys Identified Mail] signatures were inactive despite valid [asis,DKIM] configuration. The system has automatically enabled [asis,DKIM] signatures for the following [numerate,_1,domain,domains]: [list_and_quoted,_2]", domains.length, domains),
                    });
                }
            }

            /**
             *
             * Process the results of the fetchMailRecords API
             *
             * @private
             *
             * @param {Array} results batch api results [{data:...},{data:...}] where the first is the result of the has_ns_authority call, and the rest are recordType related
             * @param {Array<String>} recordTypes record types requested during the fetchMailRecords API call
             */
            Domains.prototype._processMailRecordResults = function _processMailRecordResults(results, recordTypes) {
                var batchResultItems = results.data;

                var nsAuthResult = batchResultItems.shift();

                if (nsAuthResult.data) {
                    nsAuthResult.data.forEach(this._processNSAuthResultItem, this);
                }

                recordTypes = recordTypes ? recordTypes : this.getSupportedRecordTypes();
                var processors = this.getRecordProcessors();

                recordTypes.forEach(function(recordType, index) {
                    var recordTypeResults = batchResultItems[index];
                    var processor = processors[recordType];
                    this._processRecordTypeResults(processor, recordTypeResults, recordType);

                    if (recordType === "dkim") {
                        _reportDKIMValidityCacheUpdates(recordTypeResults);
                    }
                }, this);
            };

            /**
             *
             * API Wrapper to fetch the status of all mail records for a set of domains
             *
             * @public
             *
             * @param {Array<Domain>} domains array of Domain() objects for which to fetch records
             * @param {Array<String>} recordTypes array of record types for which to fetch records
             * @returns {Promise<void>} returns the promise, but does not return an results object. That data is updated on the domain objects.
             */
            Domains.prototype.fetchMailRecords = function fetchMailRecords(domains, recordTypes) {

                var self = this;

                if (domains.length === 0) {
                    return $q.resolve();
                }

                var start = new Date();
                var startTime = start.getTime();

                var apiCall = $apiInit.init("Batch", "strict");

                var flatDomains = {};
                domains.forEach(function(domain, index) {
                    return flatDomains["domain-" + index] = domain.domain;
                });

                var commands = [];

                commands.push($apiInit.buildBatchCommandItem("DNS", "has_local_authority", flatDomains));

                recordTypes = recordTypes ? recordTypes : this.getSupportedRecordTypes();
                var processors = this.getRecordProcessors();

                recordTypes.forEach(function(recordType) {
                    var processor = processors[recordType];
                    var apiName = processor.getValidateAPI();
                    commands.push($apiInit.buildBatchCommandItem(apiName.module, apiName.func, flatDomains));
                }, this);

                apiCall.addArgument("command", commands);

                // If there’s an in-progress fetch already, then abort it
                // because we know we don’t need its data.
                if (this._fetchMailRecordsPromise) {
                    $log.debug("Canceling prior record status load");
                    this._fetchMailRecordsPromise.cancelCpCall();
                }

                this._fetchMailRecordsPromise = this.promise(apiCall);

                return this._fetchMailRecordsPromise.then(function(result) {

                    var end = new Date();
                    var endTime = end.getTime();
                    $log.debug("Updating record statuses load took " + (endTime - startTime) + "ms for " + domains.length + " domains");
                    startTime = endTime;
                    self._processMailRecordResults(result, recordTypes);
                }).finally(function() {
                    delete self._fetchMailRecordsPromise;

                    var end = new Date();
                    var endTime = end.getTime();

                    $log.debug("Updating record statuses parsing took " + (endTime - startTime) + "ms for " + domains.length + " domains");
                });
            };

            /**
             *
             * Reset the records for a given domain, and poll for the records
             *
             * @public
             *
             * @param {Array<Domain>} domains array of domains for which to fetch records
             * @param {Array<string>} recordTypes array of record types for which to fetch records
             * @returns {Promise<void>} returns the promise, but does not return an results object. That data is updated on the domain objects.
             */
            Domains.prototype.validateAllRecords = function validateAllRecords(domains, recordTypes) {

                var self = this;
                recordTypes = recordTypes ? recordTypes : self.getSupportedRecordTypes();

                // Filter out domains already queued for a reload
                domains = domains.filter(function(domain) {
                    if (domain.reloadingIn) {
                        return false;
                    }
                    return true;
                });

                domains.forEach(function(domain) {
                    domain.resetRecordLoaded();
                }, self);

                return self.fetchMailRecords(domains, recordTypes).then(function() {
                    domains.forEach(function(domain) {
                        domain.setRecordsLoaded(recordTypes);
                    }, self);
                });

            };

            /**
             *
             * Returns a list of available processors for available record types. This is the key function for disabling record types.
             *
             * @public
             *
             * @returns {Object} returns object with keys representing record types, and value representing processor objects
             */
            Domains.prototype.getRecordProcessors = function recordProcessors() {

                var processors = {
                    "dkim": DKIMRecordProcessor,
                    "spf": SPFRecordProcessor,
                    "dmarc": DMARCRecordProcessor,
                };

                if ( PAGE.skipPTRLookups === undefined || !PAGE.skipPTRLookups ) {
                    processors.ptr = PTRRecordProcessor;
                }

                return processors;
            };

            /**
             *
             * Returns the currently supported record types.
             *
             * @public
             *
             * @returns {Array<strings>} array of supported record type strings
             */
            Domains.prototype.getSupportedRecordTypes = function getSupportedRecordTypes() {
                return Object.keys(this.getRecordProcessors());
            };

            /**
             *
             * Repair SPF records for a set of domains
             *
             * @public
             *
             * @param {Array<Domain>} domains array of domains to update records for
             * @param {Array<string>} records new SPF record strings to update
             * @returns <Promise<Object>> returns a promise and then an API results object
             */
            Domains.prototype.repairSPF = function repairSPF(domains, records) {
                var processors = this.getRecordProcessors();
                var processor = processors["spf"];
                var apiName = processor.getInstallAPI();

                var flatDomains = domains.map(function(domain) {
                    return domain.domain;
                });

                var apiCall = $apiInit.init(apiName.module, apiName.func);
                apiCall.addArgument("domain", flatDomains);
                apiCall.addArgument("record", records);

                return this.promise(apiCall);
            };


            /**
             * Present a warning that the change may not be reflected
             *
             * @param {string} domain
             */
            Domains.prototype.unreflectedChangeMessage = function unreflectedchangeMessage(domain) {
                var zoneEditorURL = PAGE.zoneEditorUrl;
                var message = "";
                message += LOCALE.maketext("Because this is not an authoritative nameserver for the domain “[_1]”, the current or suggested records will not reflect your changes.", domain);
                if (zoneEditorURL) {
                    message += " ";
                    message += LOCALE.maketext("Use the [output,url,_1,Zone Editor] to ensure that the system applied your changes.", zoneEditorURL + domain);
                }

                $alertService.add({
                    message: message,
                    type: "warning",
                });
            };

            /**
             *
             * Repair DKIM records for a set of domains
             *
             * @public
             *
             * @param {Array<Domain>} domains array of domains to update records for
             * @returns <Promise<Object>> returns a promise and then an API results object
             */
            Domains.prototype.repairDKIM = function repairDKIM(domains) {
                var processors = this.getRecordProcessors();
                var processor = processors["dkim"];
                var apiName = processor.getInstallAPI();

                var flatDomains = domains.map(function(domain) {
                    return domain.domain;
                });

                var apiCall = $apiInit.init(apiName.module, apiName.func);
                apiCall.addArgument("domain", flatDomains);

                return this.promise(apiCall);
            };


            /**
             *
             * Repair DMARC records for a set of domains **current unsupported**
             *
             * @public
             *
             */
            Domains.prototype.repairDMARC = function repairDMARC(domain) {
                throw new Error("Repairing DMARC Records are not currently supported in this interface.");
            };

            /**
             *
             * Repair PTR records for a set of domains **current unsupported**
             *
             * @public
             *
             */
            Domains.prototype.repairPTR = function repairPTR(domain) {
                throw new Error("Installing PTR Records are not currently supported in this interface.");
            };

            /**
             *
             * Repair a record type for a single domain
             *
             * @public
             *
             * @param {Domain} domain domain to repair the record for
             * @param {String} recordType record type to repair
             * @param {String} newRecord new record, if setting a new record for the domain (SPF)
             * @returns {Promise<Object>} returns a promise and then the API result object
             */
            Domains.prototype.repairRecord = function repairRecord(domain, recordType, newRecord) {

                if (recordType === "spf") {
                    return this.repairSPF([domain], [newRecord]);
                } else if (recordType === "dkim") {
                    return this.repairDKIM([domain], [newRecord]);
                } else if (recordType === "ptr") {
                    return this.repairPTR([domain], [newRecord]);
                } else if (recordType === "dmarc") {
                    return this.repairDMARC([domain], [newRecord]);
                }

            };

            /**
             *
             * Called on success of a record repair
             *
             * @private
             *
             * @param {Domain} domainObj domain updated during the repair call
             * @param {String} recordType record type repaired during the repair call
             * @param {String} record resulting updated record
             */
            Domains.prototype._repairRecordSuccess = function _repairRecordSuccess(domainObj, recordType, record) {
                $alertService.success({
                    replace: false,
                    message: LOCALE.maketext("The system updated the “[_1]” record for “[_2]” to the following: [_3]", recordType.toUpperCase(), _.escape(domainObj.domain), "<pre>" + _.escape(record) + "</pre>"),
                });
            };

            /**
             *
             * Called on failure of a record repair
             *
             * @private
             *
             * @param {Domain} domainObj domain updated during the repair call
             * @param {String} recordType record type repaired during the repair call
             * @param {String} error
             */
            Domains.prototype._repairRecordFailure = function _repairRecordSuccess(domainObj, recordType, error) {
                $alertService.add({
                    type: "error",
                    replace: false,
                    message: LOCALE.maketext("The system failed to update the “[_1]” record for “[_2]” because of an error: [_3]", recordType.toUpperCase(), _.escape(domainObj.domain), _.escape(error)),
                });
            };

            Domains.prototype._interval = function _interval(func, interval, count) {
                return $interval(func, interval, count);
            };

            Domains.prototype._validateUntilSuccessComplete = function _validateUntilSuccessComplete(domainObj, successTypeRecords, waitAfter, startTime) {

                var self = this;

                $log.debug("[" + domainObj.domain + "] fetchMailRecords completed");

                var recordTypes = self.getSupportedRecordTypes();

                domainObj.setRecordsLoaded(recordTypes);
                var someFailed = successTypeRecords.some(function(recordType) {
                    return !domainObj.isRecordValid(recordType);
                });

                var timePassed = (new Date().getTime() - startTime) / 1000;

                $log.debug("[" + domainObj.domain + "] time passed since records set: " + timePassed);

                // If not, double waitAfter and wait that long to check again
                if (someFailed && timePassed < 120) {
                    $log.debug("[" + domainObj.domain + "] some failed, wait " + (waitAfter) + "s, and then try again.");

                    // Only bother alerting if it's been more than 5 seconds since the first call.
                    // A safety for the I/O issue found in COBRA-8775
                    if (timePassed > 5) {
                        $alertService.add({
                            type: "info",
                            replace: true,
                            message: LOCALE.maketext("The server records have not updated after [quant,_1,second,seconds]. The system will try again in [quant,_2,second,seconds].", Math.floor(timePassed), Math.floor(waitAfter)),
                        });
                    }

                    domainObj.resetRecordLoaded();

                    domainObj.reloadingIn = waitAfter;

                    return self._interval(function() {
                        domainObj.reloadingIn--;
                    }, 1000, waitAfter).then(function() {
                        domainObj.reloadingIn = 0;
                        waitAfter *= 2;
                        $log.debug("[" + domainObj.domain + "] done waiting, trying again.");
                        return self.validateUntilSuccess(domainObj, successTypeRecords, waitAfter, startTime);
                    });
                } else {

                    if (!someFailed) {
                        $log.debug("[" + domainObj.domain + "] all records fixed.");
                        $alertService.success({
                            replace: true,
                            message: LOCALE.maketext("The system successfully updated the [asis,DNS] records."),
                        });
                    } else if (timePassed > 120) {
                        $log.debug("[" + domainObj.domain + "] more than 120s was taken to validate the change.");
                        $alertService.add({
                            type: "warning",
                            replace: true,
                            message: LOCALE.maketext("The system cannot verify that the record updated after 120 seconds."),
                        });
                    }

                    // If records are valid, we're done
                    domainObj.setRecordsLoaded(self.getSupportedRecordTypes());
                }
            };


            Domains.prototype.validateUntilSuccess = function validateUntilSuccess(domain, successTypeRecords, waitAfter, startTime) {
                var self = this;

                var domainObj = this._getDomainObject(domain);
                var recordTypes = self.getSupportedRecordTypes();

                $log.debug("[" + domain.domain + "] begin validateUntilSuccess @" + (waitAfter) + "s");

                return self.fetchMailRecords([domainObj], recordTypes).then(function() {
                    return self._validateUntilSuccessComplete(domainObj, successTypeRecords, waitAfter, startTime);
                });
            };

            Domains.prototype.getDomainZoneObject = function getDomainZoneObject(domain) {
                var domainObj = this._getDomainObject(domain);
                return this.findZoneByName(domainObj.zone) || false;
            };

            Domains.prototype._repairDomainComplete = function _repairDomainComplete(result, domain, recordTypes, records, skipValidation) {

                $log.debug("[" + domain.domain + "] beginning _repairDomainComplete.");

                var self = this;

                if (result.data) {
                    var recordsThatSucceeded = [];
                    result.data.forEach(function _processRecordResultObj(recordResultObj, index) {
                        var recordType = recordTypes[index];
                        var record = records[index];
                        var domainResultObj = recordResultObj.data[0];

                        $log.debug("[" + domainResultObj.domain + "] being parsed in _repairDomainComplete status: [" + domainResultObj.status + "].");

                        if (domainResultObj.status.toString() !== "1") {
                            this._repairRecordFailure(domain, recordType, domainResultObj.msg);
                        } else {
                            recordsThatSucceeded.push(recordType);
                            this._repairRecordSuccess(domain, recordType, record);
                        }
                    }, this);

                    if (skipValidation) {
                        $log.debug("[" + domain.domain + "] does not have NS Authority. Do not continue validation check");
                        return;
                    }

                    if (recordsThatSucceeded.length) {
                        $log.debug("[" + domain.domain + "] some records succeeeded.", recordsThatSucceeded.join(","));

                        var startTime = new Date().getTime();

                        return self.validateUntilSuccess(domain, recordsThatSucceeded, 5, startTime);
                    } else {
                        return self.validateAllRecords([domain]);
                    }
                }

            };

            Domains.prototype.repairDomain = function repairDomain(domain, recordTypes, records, skipValidation) {

                recordTypes = recordTypes.slice();
                records = records.slice();

                var self = this;

                var domainObj = this._getDomainObject(domain);

                // Lock the Zone
                var zoneObj = this.findZoneByName(domainObj.zone);
                zoneObj.lock(domainObj);
                $log.debug("[" + domainObj.domain + "] locking zone: ", zoneObj.zone);

                $log.debug("[" + domainObj.domain + "] beginning repairDomain.");

                // Only reset it if we have auth, because otherwise it won't come back
                if (!skipValidation) {
                    domainObj.resetRecordLoaded();
                }

                $log.debug("[" + domainObj.domain + "] resetting loaded records.");

                // Start the Repair - Batch is serial, so this is fine to do
                var apiCall = $apiInit.init("Batch", "strict");
                var processors = self.getRecordProcessors();

                var commands = recordTypes.map(function(recordType, index) {
                    var record = records[index];
                    var processor = processors[recordType];
                    var apiName = processor.getInstallAPI();
                    $log.debug("[" + domainObj.domain + "] adding batch command for “" + recordType + "”.");
                    if ( window.mixpanel ) {
                        window.mixpanel.track("Email-Deliverability-Repair-Record", { type: recordType });
                    }

                    if (recordType === "dmarc") {
                        return $apiInit.buildBatchCommandItem(apiName.module, apiName.func, { "domain": domainObj.domain, policy: record });
                    }

                    return $apiInit.buildBatchCommandItem(apiName.module, apiName.func, { "domain": domainObj.domain, record: record });
                });

                apiCall.addArgument("command", commands);

                return self.promise(apiCall).then(function(result) {
                    $log.debug("[" + domainObj.domain + "] batch command completed.");
                    zoneObj.unlock();
                    return self._repairDomainComplete(result, domainObj, recordTypes, records, skipValidation);
                });
            };

            /**
             *
             * Process the API result for fetchPrivateDKIMKey
             *
             * @private
             *
             * @param {Object} result API results object {data:...}
             * @returns first result of API results.data array
             */
            Domains.prototype._processGetPrivateDKIMKey = function _processGetPrivateDKIMKey(result) {
                return result.data.pop();
            };

            /**
             *
             * Get the private DKIM key for a domain
             *
             * @public
             *
             * @param {Domain} domain domain to request the DKIM key for
             * @returns {Promise<Object>} DKIM Key object {pem:...,domain:...}
             */
            Domains.prototype.fetchPrivateDKIMKey = function fetchPrivateDKIMKey(domain) {
                var domainObj = this._getDomainObject(domain);

                var apiCall = $apiInit.init("EmailAuth", "fetch_dkim_private_keys");
                apiCall.addArgument("domain", domainObj.domain);

                return this.promise(apiCall).then(this._processGetPrivateDKIMKey);
            };

            /**
             *
             * Get the top most current record for a domain and record type
             *
             * @public
             *
             * @param {Domain} domain domain to get the record from
             * @param {String} recordType record type to fetch the record for
             * @returns {String} returns the string record or an empty string
             */
            Domains.prototype.getCurrentRecord = function getCurrentRecord(domain, recordType) {

                var domainObj = this._getDomainObject(domain);
                return domainObj.getCurrentRecord(recordType);

            };

            Domains.prototype.getNoAuthorityMessage = function getNoAuthorityMessage(domainObj, recordType) {
                var nameserversHtml = domainObj.nameservers.map( _.escape );

                var message;

                if (domainObj.nameservers.length) {
                    message = LOCALE.maketext("This system does not control [asis,DNS] for the “[_1]” domain.", domainObj.domain);
                } else {
                    message = LOCALE.maketext("This system does not control [asis,DNS] for the “[_1]” domain and the system did not find any authoritative nameservers for this domain.", domainObj.domain);
                }

                if (recordType === "spf" || recordType === "dkim") {

                    // Handle differently for spf
                    message += " ";
                    message += "<strong>";
                    message += LOCALE.maketext("You can install the suggested “[_1]” record locally. However, this server is not the authoritative nameserver. If you install this record, this change will not be effective.", recordType.toUpperCase());
                    message += "</strong>";
                }

                message += " ";

                if (domainObj.nameservers.length) {
                    message += LOCALE.maketext("Contact the person responsible for the [list_and_quoted,_3] [numerate,_2,nameserver,nameservers] and request that they update the “[_1]” record with the following:", recordType.toUpperCase(), domainObj.nameservers.length, nameserversHtml);
                } else {
                    message += LOCALE.maketext("Contact your domain registrar to verify this domain’s registration.");
                }

                return message;

            };

            /**
             *
             * API Wrapper to obtain the helo record for a domain
             *
             * @param {Domain} domain domain to request the helo record for
             * @returns {Promise} get_mail_helo_ip promise
             */
            Domains.prototype.fetchMailHeloIP = function fetchMailHeloIP(domain) {

                var domainObj = this._getDomainObject(domain);

                var apiCall = $apiInit.init("EmailAuth", "get_mail_helo_ip");
                apiCall.addArgument("domain", domainObj.domain);

                return this.promise(apiCall).then(this._processMailHeloResult);

            };

            Domains.prototype.localDKIMExists = function localDKIMExists(domain) {
                var recordType = "dkim";

                if (domain.isRecordValid(recordType)) {
                    return true;
                }

                var record = domain.getSuggestedRecord(recordType);
                if (record.value) {
                    return true;
                }
                return false;
            };

            Domains.prototype.ensureLocalDKIMKeyExists = function ensureLocalDKIMKeyExists(domain) {
                var self = this;
                var apiCall = $apiInit.init("EmailAuth", "ensure_dkim_keys_exist");

                apiCall.addArgument("domain", domain.domain);

                return self.promise(apiCall).then(function(results) {
                    var data = results.data;
                    var domainResult = data.pop();
                    if (domainResult.status.toString() === "1") {
                        return self.fetchMailRecords([domain]);
                    } else {
                        return $alertService.add({
                            type: "error",
                            message: _.escape(domainResult.msg),
                        });
                    }
                });
            };

            // To be called whenever a view changes so that pending
            // “load” API calls can be canceled or abandoned.
            Domains.prototype.markViewLoad = function() {
                if (this._fetchMailRecordsPromise) {
                    this._fetchMailRecordsPromise.cancelCpCall();
                }
            };

            return new Domains();
        };

        SERVICE_INJECTABLES.push(SERVICE_FACTORY);

        var app = angular.module(MODULE_NAMESPACE, MODULE_REQUIREMENTS);
        app.value("PAGE", PAGE);
        app.value("DOMAIN_TYPE_CONSTANTS", domainFactory.getTypeConstants());
        app.factory(SERVICE_NAME, SERVICE_INJECTABLES);

        return {
            "class": SERVICE_FACTORY,
            "namespace": MODULE_NAMESPACE,
        };
    }
);
Back to Directory File Manager