HEX
Server: Apache
System: Linux hz.vslconceptsdomains.com 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64
User: dkfounda (3233)
PHP: 8.1.34
Disabled: exec,passthru,shell_exec,system
Upload Files
File: //usr/local/mailchannels/services/OutboundSMTPServiceImpl.php
<?php
namespace MailChannels;

use MailChannels\WHM\SPFRecordBuilder;
use MailChannels\WHM\TXTRecordBuilder;
use Net_DNS2_Exception;

class OutboundSMTPServiceImpl implements OutboundSMTPService {
    const UNEXPECTED_WHM_ERROR = "An unexpected error occurred while communicating with the whm api.";
    const UNEXPECTED_UAPI_ERROR = "An unexpected error occurred while communicating with the UAPI.";

    private $eximConfigurer;
    private $whmAPI;
    private $uAPI;
    private $resolver;
    private $spfCountMC;

    public function __construct(EximConfigurer $eximConfigurer, WHMAPI1 $whmAPI) {
        $this->eximConfigurer = $eximConfigurer;
        $this->whmAPI = $whmAPI;
        $this->resolver = App::getResolver();
        $this->uAPI = App::getUAPI();
    }

    /**
     * @return bool
     * @throws FileNotFoundException
     * @throws OutboundConfigurationException|StorageNotSetException
     * @throws UAPIException
     */
    public function configureMailServer() {
        App::getLogger()->info("Configuring outbound mail");
        $config = App::getStorage()->getOutboundConfig();
        $eximConfigurer = $this->eximConfigurer;

        try {
            if ($eximConfigurer->hasMCRouter()) {
                $eximConfigurer->removeMCRouter();
            }

            if (!$eximConfigurer->hasMCPostMailCount()) {
                $eximConfigurer->addMCPostMailCount();
            }

            if (!$eximConfigurer->hasMCTransport()) {
                try {
                    $limit = $this->whmAPI->getTweakSetting("message_linelength_limit", "Mail");
                } catch (WHMApiPrivilegeException $e) {
                    App::getLogger()->warning("Couldn't get message_linelength_limit. 'All' API key must be enabled for detection. Defaulting to 2048");
                    $limit = 2048;
                }
                $eximConfigurer->addMCTransport($limit);
            }

            if (!$eximConfigurer->hasMCAuth()) {
                $eximConfigurer->addMCAuth();
            }

            $eximConfigurer->rebuildExim();

            // adding or removing the X-MC-MailingList header must happen after the call to rebuildExim, otherwise the
            // changes will be lost
            if ($config->enableMailManHeaders()) {
                $this->addMailManHeaders();
            } else {
                $this->removeMailManHeaders();
            }

            $eximConfigurer->restartExim();
            if ($config->autoUpdateSPFRecords()) {
                try {
                    $domains = $this->getWHMDomains(true);
                } catch (UAPIException $e) {
                    throw $e;
                } catch (\Exception $e) {
                    throw new OutboundSMTPServiceInternalErrorException(self::UNEXPECTED_WHM_ERROR, $e->getCode(), $e);
                }
                $errors = $this->addMCSPFForDomains($domains);
                foreach ($errors as $domain => $e) {
                    App::getLogger()->error("Couldn't add mailchannels SPF record for '$domain': " . $e[0]->getMessage());

                    App::getOutboundFailureLogger()->error(
                        "Couldn't add SPF record",
                        array(
                            'domain' => $e[1],
                            'error' => $e[0]->getMessage(),
                        )
                    );
                }
                if ($config->enableDomainLockdown()){
                    $this->addMCLockdownForDomains($domains);
                }
            }

            return $this->isMailServerConfigured();
        } catch (EximException $e) {
            throw new OutboundConfigurationException("an error occurred while configuring the outbound mail server", $e->getCode(), $e);
        } catch (OutboundSMTPServiceInternalErrorException $e) {
            throw new OutboundConfigurationException("error updating SPF records", $e->getCode(), $e);
        }
    }

    public function addMailManHeaders() {
        $eximConfigurer = $this->eximConfigurer;
        $eximConfigurer->addMailManHeaders();
    }

    public function removeMailManHeaders() {
        $eximConfigurer = $this->eximConfigurer;
        $eximConfigurer->removeMailManHeaders();
    }

    public function hasMailManHeaders() {
        $eximConfigurer = $this->eximConfigurer;
        return $eximConfigurer->hasMailManHeaders();
    }

    /**
     * @param string $file
     * @throws OutboundConfigurationException
     */
    public function retryFailedOutboundDomains($file) {
        $domains = $this->readOutboundFailures($file);
        // Delete the log file so we aren't continuously retrying domains. New errors will be logged appropriately
        file_put_contents($file, "");
        $errors = $this->addMCSPFForDomains($domains);
        foreach ($errors as $domain => $e) {
            App::getLogger()->error("error retrying SPF add for '$domain': " . $e[0]->getMessage());

            App::getOutboundFailureLogger()->error(
                "Couldn't add SPF record",
                array(
                    'domain' => $e[1],
                    'error' => $e[0]->getMessage(),
                )
            );
        }
    }

    /**
     * @param string $filePath
     * @return array WHM\Domain
     * @throws OutboundConfigurationException
     */
    private function readOutboundFailures($filePath) {
        $domains = array();
        if (file_exists($filePath)) {
            $handle = fopen($filePath, "r");
            if ($handle) {
                while ( ($line = fgets($handle) ) !== false) {
                    $domains[] = $this->decodeLine($line);
                }
                fclose($handle);
            } else {
                App::getLogger()->info("no outbound failures found");
            }
        } else {
            App::getLogger()->info("$filePath not found");
        }
        return $domains;
    }

    /**
     * @param string $line
     * @return WHM\Domain
     * @throws OutboundConfigurationException
     */
    private function decodeLine($line) {
        $decoded = json_decode($line, true);
        if (!$decoded) {
            throw new OutboundConfigurationException("could not decode json: $line");
        }
        if (!isset($decoded['context']) || ! isset($decoded['context']['domain']) || !isset($decoded['context']['domain']['domain']) || !isset($decoded['context']['domain']['type'])) {
            throw new OutboundConfigurationException("missing required context, domain or type fields from $line");
        }
        $domain = $decoded['context']['domain'];
        $domainName = $domain['domain'];
        $type = $domain['type'];
        $domainBuilder = (new WHM\DomainBuilder())->setType($type)->setDomain($domainName);
        if (isset($domain['parent'])) {
            $domainBuilder->setParentDomain($domain['parent']);
        }
        return $domainBuilder->build();
    }

    /**
     * @return bool
     * @throws OutboundConfigurationException
     */
    public function isMailServerConfigured() {
        $eximConfigurer = $this->eximConfigurer;
        try {
            return $eximConfigurer->isConfigured();
        } catch (EximException $e) {
            throw new OutboundConfigurationException("an error occurred checking the mail server configuration status", $e->getCode(), $e);
        }
    }

    /**
     * @return bool
     * @throws \Exception
     */
    public function isEditableForMCConfig() {
        $config = App::getConfig();
        return $config->isEditableForMCConfig($config::EXIM_CONF_LOCAL);
    }

    /**
     * @throws OutboundConfigurationException
     * @throws StorageNotSetException
     * @throws UAPIException
     */
    public function unConfigureMailServer() {
        App::getLogger()->info("Removing outbound configuration");
        $config = App::getStorage()->getOutboundConfig();
        $eximConfigurer = $this->eximConfigurer;

        try {
            if ($eximConfigurer->hasMCTransport()) {
                $eximConfigurer->removeMCTransport();
            }

            if ($eximConfigurer->hasMCRouter()) {
                $eximConfigurer->removeMCRouter();
            }

            if ($eximConfigurer->hasMCPostMailCount()) {
                $eximConfigurer->removeMCPostMailCount();
            }

            if ($eximConfigurer->hasMCAuth()) {
                $eximConfigurer->removeMCAuth();
            }

            $eximConfigurer->rebuildExim();

            if ($config->autoUpdateSPFRecords()) {
                $errors = $this->removeMCSPFForAllDomains();
                foreach ($errors as $domain => $e) {
                    App::getLogger()->error("Couldn't remove mailchannels SPF record for domain '$domain': " . $e->getMessage());
                }
            }
        } catch (EximException $e) {
            throw new OutboundConfigurationException("an error occurred while un-configuring MailChannels from the outbound mailserver", $e->getCode(), $e);
        } catch (OutboundSMTPServiceInternalErrorException $e) {
            throw new OutboundConfigurationException("error removing SPF records", $e->getCode(), $e);
        }
    }

    /**
     * @return array
     * @throws OutboundSMTPServiceInternalErrorException
     * @throws UAPIException
     */
    public function addMCSPFForAllDomains() {
        App::getLogger()->info("Adding the mailchannels spf records");
        try {
            $domains = $this->getWHMDomains(true);
        } catch (UAPIException $e) {
            throw $e;
        } catch (\Exception $e) {
            throw new OutboundSMTPServiceInternalErrorException(self::UNEXPECTED_WHM_ERROR, $e->getCode(), $e);
        }

        return $this->addMCSPFForDomains($domains);
    }

    /**
     * @param $domains array WHM\Domain
     * @return array domain => [error, WHM\Domain domain]
     */
    private function addMCSPFForDomains($domains) {
        $errors = array();
        $this->setMCSPFCount();
        foreach ($domains as $domain) {
            try {
                if ($domain->getType() === WHM\Domain::TYPE_SUB) {
                    $this->addMCSPFForSubDomain($domain);
                } else {
                    $this->addMCSPFForDomain($domain->getDomain());
                }
            } catch (DNSRecordException $e) {
                $errors[$domain->getDomain()] = [$e, $domain];
            } catch (OutboundSMTPServiceInternalErrorException $e) {
                $errors[$domain->getDomain()] = [$e, $domain];
            } catch (Net_DNS2_Exception $e) {
                $errors[$domain->getDomain()] = [$e, $domain];
            } catch (MissingZoneException $e) {
                $errors[$domain->getDomain()] = [$e, $domain];
            } catch (UnknownDNSRecordClass $e) {
                $errors[$domain->getDomain()] = [$e, $domain];
            } catch (ExceedsDNSLookupLimitException $e) {
                $errors[$domain->getDomain()] = [$e, $domain];
            }
        }

        return $errors;
    }

    /**
     * @param $domainName
     * @throws MissingZoneException
     * @throws UnknownDNSRecordClass
     * @throws DNSRecordException
     * @throws \Net_DNS2_Exception
     * @throws OutboundSMTPServiceInternalErrorException
     * @throws ExceedsDNSLookupLimitException
     */
    public function addMCSPFForDomain($domainName) {
        App::getLogger()->info("Adding the mc spf record for $domainName");
        $config = App::getConfig();
        $outboundConfig = App::getStorage()->getOutboundConfig();

        try {
            $spfRecord = $this->spfRecordForDomain($domainName);
        } catch (WHMApiBadStatusException $e) {
            $message = "could not get the spf record for $domainName";
            throw new OutboundSMTPServiceInternalErrorException($message, $e->getCode(), $e);
        }

        if (!$spfRecord) {
            App::getLogger()->info("Not adding MC SPF records for domain $domainName, domain has no existing SPF record and this may cause loss of deliverability");
        } else {
            if (!$spfRecord->matches(preg_quote('include:' . $config::MC_SPF_INCLUDE))) {

                if ($outboundConfig->enforceDNSLimit()) {
                    $exceedCheck = $this->willSPFExceedDNSLimitMC($domainName);

                    if($exceedCheck[0]) {
                        $message = "Adding our spf records to $domainName would exceed the DNS lookup limit adding $exceedCheck[1] to $exceedCheck[2]";
                        throw new ExceedsDNSLookupLimitException($message);
                    }
                }


                try {
                    $zone = $this->whmAPI->dumpZone($domainName);
                } catch (WHMApiBadStatusException $e) {
                    $message = "could not get the zone record for $domainName";
                    throw new OutboundSMTPServiceInternalErrorException($message, $e->getCode(), $e);
                }

                // while unlikely, it's possible that spfRecordForDomain($domainName) returns a record,
                // and $zone->getRootSPFRecord() does not. to avoid PHP Fatal errors, we'll throw an
                // exception so the domain in question gets logged and the user can continue setting
                // up.
                if (!$zone || !$zone->getRootSPFRecord()) {
                    $text = $spfRecord->getText();
                    $message = "zone getRootSPFRecord is null for $domainName, but was found outside of the whm api (got: $text)";
                    throw new EditDNSRecordException($message);
                }
                $newSPFRecord = (new SPFRecordBuilder())
                    ->fromTXTRecord($zone->getRootSPFRecord())
                    ->addTerms('include:' . $config::MC_SPF_INCLUDE)
                    ->build();

                try {
                    $this->whmAPI->editZoneRecord($newSPFRecord);
                } catch (WHMApiBadStatusException $e) {
                    $name = $newSPFRecord->getName();
                    $message = "an error occurred while trying to edit an SPF Record (name: $name)";

                    throw new EditDNSRecordException($message, $e->getCode(), $e);
                }
            }
        }
    }

    /**
     * @param WHM\Domain
     * @throws MissingZoneException
     * @throws UnknownDNSRecordClass
     * @throws DNSRecordException
     * @throws Net_DNS2_Exception
     * @throws OutboundSMTPServiceInternalErrorException
     */
    public function addMCSPFForSubDomain($domain) {
        $subDomainName = $domain->getDomain();
        $mainDomain = $domain->getParentDomain();
        App::getLogger()->info("Adding the mc spf record for subdomain $subDomainName, the main domain is $mainDomain");
        $config = App::getConfig();

        $mainDomainZone = $this->whmAPI->dumpZone($mainDomain);

        $subDomainTXTRecord = null;
        $mainTXTRecords = $mainDomainZone->getTXTRecords();
        foreach ($mainTXTRecords as $txtRecord) {
            // getName returns domain names that end with a '.'
            if ($txtRecord->getName() == $subDomainName . ".") {
                $subDomainTXTRecord = $txtRecord;
            }
        }
        // only edit an existing record if it does not have "include: mainDomain"; it's redundant
        // to add the mailchannels record. it will be added to the main domain
        if ($subDomainTXTRecord != null) {
            $record = $subDomainTXTRecord->getText();
            $mainDomainSPF = preg_quote('include:' . $mainDomain);
            $mcSPF = preg_quote('include:' . $config::MC_SPF_INCLUDE);

            $containsMainDomainSPF = (bool) preg_match("/$mainDomainSPF/", $record);
            $containsMCSPF = (bool) preg_match("/$mcSPF/", $record);

            if ( !$containsMainDomainSPF && !$containsMCSPF ) {
                App::getLogger()->info("editing the spf record for $subDomainName to include the mailchannels record");
                $newSPFRecord = (new SPFRecordBuilder())
                    ->fromTXTRecord($subDomainTXTRecord)
                    ->addTerms('include:' . $config::MC_SPF_INCLUDE)
                    ->build();

                try {
                    $this->whmAPI->editZoneRecord($newSPFRecord);
                } catch (WHMApiBadStatusException $e) {
                    $message = "error editing the spf record for $subDomainName using main domain $mainDomain";
                    throw new EditDNSRecordException($message, $e->getCode(), $e);
                }
            } else {
                App::getLogger()->info("the spf record for $subDomainName is up to date");
            }
        } else {
            // a subdomain created via cpanel should always have an spf record.
            App::getLogger()->info("missing an existing spf record for $subDomainName, skipping");
        }
    }

    private function addMCLockdownForDomains($domains){
        $errors = array();
        foreach ($domains as $domain) {
            try {
                $this->addMCLockdownForDomain($domain);
            } catch (\Exception $e) {
                $errors[$domain->getDomain()] = [$e, $domain];
            }
        }

        return $errors;
    }

    /**
     * @param \MailChannels\Plugin\Domain domain
     * @throws AddDNSRecordException
     * @throws WHMApiBadStatusException
     * @throws UAPIException
     * @throws WHMApiNotFoundException
     */
    private function addMCLockdownForDomain($domain){
        $domainName = $domain->getDomain();
        App::getLogger()->debug("Adding Domain Lockdown record for ". $domainName);

        if ($this->whmAPI->isDemoDomain($domain)) {
            App::getLogger()->debug("$domainName is a demo domain, not adding Domain Lockdown record");
            return;
        }
        if ($this->whmAPI->isDomainSuspended($domainName)) {
            App::getLogger()->info("$domainName is suspended, cannot add Domain Lockdown record");
            return;
        }


        if ($this->whmAPI->isUsingLocalDNS($domain)){
            $config = App::getStorage()->getOutboundConfig();
            // note that "character strings" shouldn't be more than 256 characters
            // (see https://datatracker.ietf.org/doc/html/rfc1035 for more details)
            if (strlen($config->getUsername()) > 251){
                App::getLogger()->error("Username is too long to be stored in TXT record, will not be adding Domain Lockdown record for auth: ".$config->getUsername());
                return;
            }
            $record = (new TXTRecordBuilder())
            ->setDomain($domain->getDomain())
            ->setName("_mailchannels.".$domainName)
            ->setText("v=mc1 auth=".$config->getUsername())
            ->build();
            try {
                $this->whmAPI->addZoneRecord($record);
                App::getLogger()->debug("Added DomainLockdown record for ".$domainName);
            } catch (UnknownDNSRecordClass $e) {
                App::getLogger()->error("Failed to add DomainLockdown record for domain '".$domainName."':".$e->getMessage());
                throw new AddDNSRecordException("an error occurred while trying to add DomainLockdown record ", $e->getCode(), $e);
            } catch (WHMApiBadStatusException $e) {
                App::getLogger()->error("Failed to add DomainLockdown record for domain '".$domainName."':".$e->getMessage());
                throw new AddDNSRecordException("an error occurred while trying to add DomainLockdown record ", $e->getCode(), $e);
            }
        } else {
            // DNS isn't kept locally, so we can't add the record
            App::getLogger()->debug("domain is not using local DNS, will not add DomainLockdown TXT record: ". $domainName);
        }
    }

    /**
     * @return array map of domain names to exceptions
     * @throws OutboundSMTPServiceInternalErrorException
     * @throws UAPIException
     */
    public function removeMCSPFForAllDomains() {
        App::getLogger()->info("Removing the mailchannels spf records");
        try {
            $domains = $this->getWHMDomains(true);
        } catch (UAPIException $e) {
            throw $e;
        } catch (\Exception $e) {
            throw new OutboundSMTPServiceInternalErrorException(self::UNEXPECTED_WHM_ERROR, $e->getCode(), $e);
        }

        $errors = array();

        foreach ($domains as $domain) {
            try {
                if ($domain->getType() === WHM\Domain::TYPE_SUB) {
                    $this->removeMCSPFForSubDomain($domain);
                } else {
                    $this->removeMCSPFForDomain($domain->getDomain());
                }
            } catch (DNSRecordException $e) {
                $errors[$domain->getDomain()] = $e;
            } catch (OutboundSMTPServiceInternalErrorException $e) {
                $errors[$domain->getDomain()] = $e;
            } catch (Net_DNS2_Exception $e) {
                $errors[$domain->getDomain()] = $e;
            } catch (MissingZoneException $e) {
                $errors[$domain->getDomain()] = $e;
            } catch (UnknownDNSRecordClass $e) {
                $errors[$domain->getDomain()] = $e;
            } catch (WHMApiBadStatusException $e) {
                $errors[$domain->getDomain()] = $e;
            }
        }

        return $errors;
    }

    /**
     * @param $domainName
     * @throws MissingZoneException
     * @throws UnknownDNSRecordClass
     * @throws \Net_DNS2_Exception
     * @throws OutboundSMTPServiceInternalErrorException
     * @throws DNSRecordException
     */
    public function removeMCSPFForDomain($domainName) {
        App::getLogger()->info("Removing the mc spf record for $domainName");
        $config = App::getConfig();
        try {
            $spfRecord = $this->spfRecordForDomain($domainName);

            if ($spfRecord) {
                if ($spfRecord->matches(preg_quote('include:' . $config::MC_SPF_INCLUDE))) {
                    $zone = $this->whmAPI->dumpZone($domainName);

                    $spfRecord = $zone->getRootSPFRecord();

                    $newTerms = array_filter($spfRecord->getTerms(), function($value) use ($config) {
                        return !preg_match('/' . preg_quote('include:' . $config::MC_SPF_INCLUDE) . '/', $value);
                    });

                    $newSpfRecord = (new SPFRecordBuilder())->fromTXTRecord($spfRecord)->setText(implode(' ', $newTerms))->build();

                    try {
                        $this->whmAPI->editZoneRecord($newSpfRecord);
                    } catch (WHMApiBadStatusException $e) {
                        $name = $newSpfRecord->getName();
                        throw new EditDNSRecordException("an error occurred while trying to edit an SPF Record (name: $name)", $e->getCode(), $e);
                    }
                }
            }
        } catch (WHMApiBadStatusException $e) {
            throw new OutboundSMTPServiceInternalErrorException(self::UNEXPECTED_WHM_ERROR, $e->getCode(), $e);
        }
    }

    /**
     * @param WHM\Domain
     * @throws MissingZoneException
     * @throws UnknownDNSRecordClass
     * @throws \Net_DNS2_Exception
     * @throws OutboundSMTPServiceInternalErrorException
     * @throws DNSRecordException|WHMApiBadStatusException
     */
    public function removeMCSPFForSubDomain($domain) {
        $subDomainName = $domain->getDomain();
        $mainDomain = $domain->getParentDomain();
        App::getLogger()->info("Removing the mc spf record for subdomain $subDomainName, the main domain is $mainDomain");
        $config = App::getConfig();

        $mainDomainZone = $this->whmAPI->dumpZone($mainDomain);

        $subDomainTXTRecord = null;
        $mainTXTRecords = $mainDomainZone->getTXTRecords();
        foreach ($mainTXTRecords as $txtRecord) {
            // getName returns domain names that end with a '.'
            if ($txtRecord->getName() == $subDomainName . ".") {
                $subDomainTXTRecord = $txtRecord;
            }
        }

        if ($subDomainTXTRecord != null) {
            $record = $subDomainTXTRecord->getText();
            $mcSPF = preg_quote('include:' . $config::MC_SPF_INCLUDE);
            $containsMCSPF = (bool) preg_match("/$mcSPF/", $record);

            if ($containsMCSPF) {
                App::getLogger()->info("editing spf record for $subDomainName to remove the mailchannels record");

                $newTerms = array_filter($subDomainTXTRecord->getTerms(), function($value) use ($config) {
                    return !preg_match('/' . preg_quote('include:' . $config::MC_SPF_INCLUDE) . '/', $value);
                });

                $newSPFRecord = (new SPFRecordBuilder())->fromTXTRecord($subDomainTXTRecord)
                    ->setText(implode(' ', $newTerms))
                    ->build();

                try {
                    $this->whmAPI->editZoneRecord($newSPFRecord);
                } catch (WHMApiBadStatusException $e) {
                    $message = "error removing the mailchannels spf record for $subDomainName using main domain $mainDomain";
                    throw new EditDNSRecordException($message, $e->getCode(), $e);
                } catch (UnknownDNSRecordClass $e) {
                    $message = "unknown dns record class while removing the mailchannels spf record for $subDomainName using main domain $mainDomain";
                    throw new EditDNSRecordException($message, $e->getCode(), $e);
                }
            } else {
                App::getLogger()->info("the spf record for $subDomainName does not have the mailchannels spf record");
            }
        } else {
            App::getLogger()->info("missing an existing spf record for $subDomainName, not removing the mailchannels spf");
        }
    }

    /**
     * @param $username
     * @throws OutboundSMTPServiceInternalErrorException
     * @throws UAPIException
     * @return array of WHM Domains
     */
    public function getAllDomainsForUser($username){
        try {
            return $this->uAPI->listDomains($username);
        } catch (UAPIException $e) {
            throw $e;
        } catch (\Exception $e) {
            App::getLogger()->debug("Couldn't retrieve domains for user '$username': " . $e->getMessage(). "\nTrace: " . $e->getTraceAsString());
            App::getLogger()->error("Couldn't retrieve domains for user '$username': " . $e->getMessage());
            throw new OutboundSMTPServiceInternalErrorException(self::UNEXPECTED_UAPI_ERROR, $e->getCode(), $e);
        }
    }

    public function toggleDkim($enable){
        App::getLogger()->info("toggling dkim: ".$enable);
        $eximConfigurer = $this->eximConfigurer;
        $eximConfigurer->toggleDkim($enable);
        $eximConfigurer->rebuildExim();
    }

    /**
     * @param $domainName
     * @return WHM\SPFRecord
     * @throws MissingZoneException
     * @throws WHMApiBadStatusException
     * @throws UnknownDNSRecordClass
     */
    private function spfRecordForDomain($domainName) {
        $zone = $this->whmAPI->dumpZone($domainName);
        return $zone->getRootSPFRecord();
    }

    /**
     * @param bool $includeAddonAndParked
     * @return array
     * @throws InboundAPIKeyNotSetException
     * @throws UAPIException
     * @throws WHMApiBadStatusException
     */
    private function getWHMDomains($includeAddonAndParked=false) {
        App::getLogger()->info("Retrieving WHM accounts, including addon and parked domains: $includeAddonAndParked");
        $accounts = $this->whmAPI->listAccounts();
        $domains = array();

        if ($includeAddonAndParked) {
            foreach ($accounts as $account) {
                $userName = $account->getUsername();
                $accountDomainName = $account->getDomain();

                // demo accounts "allow users to navigate features of cPanel & WHM. The users will not be able to make changes
                // to any files or settings." we will not include demo accounts in the domains to be managed by our plugin
                $isDemoAccount = $this->uAPI->isDemoAccount($userName);
                if ($isDemoAccount) {
                    App::getLogger()->info("skipping $accountDomainName, it is a demo account");
                    continue;
                }

                $userDomains = $this->uAPI->listDomains($userName);
                $domains = array_merge($domains, $userDomains);
            }
        } else {
            $domainBuilder = (new WHM\DomainBuilder())->setType(WHM\Domain::TYPE_MAIN);
            foreach ($accounts as $account) {
                $domains[] = $domainBuilder
                    ->setDomain($account->getDomain())
                    ->setUser($account->getUsername())
                    ->setIsSuspended($account->isSuspended())
                    ->build();
            }
        }

        return $domains;
    }

    // counts the number of DNS queries to fully resolve an spf record for $domainName
    // does not do any validation of the spf record
    public function countDNSLookups($domainName) {
        try {
            $spfRecord = $this->getSPFRecord($domainName);
            return $this->SPFRecordDNSCount($spfRecord);
        } catch (\Exception $e) {
            $message = "could not get the spf record for $domainName: $e";
            throw new OutboundSMTPServiceInternalErrorException($message, $e->getCode(), $e);
        }
    }

    //Separate the functionality from a user domain/spfrecord perspective so can use the same logic for mc spfrecord
    //mxloop isn't needed in this case as there's no domain tied to an mx tag
    //the include loop should include all relay.mailchannels.net domains needed for accurate count
    public function SPFRecordDNSCount($spfRecord) {
        $includes = $this->getIncludes($spfRecord);
        $a = $this->getA($spfRecord);
        $mx = $this->getMX($spfRecord);
        $ptr = $this->getPtr($spfRecord);
        $exists = $this->getExists($spfRecord);
        $redirects = $this->getRedirect($spfRecord);

        return count($includes)
            + count($a)
            + count($mx)
            + count($ptr)
            + count($exists)
            + count($redirects)
            + $this->countRecursive($includes)
            + $this->countMXLookups($mx)
            + $this->countRecursive($redirects);
    }


    private function setMCSPFCount() {
        $config = App::getConfig();
        $this->spfCountMC = $this->SPFRecordDNSCount($config::MC_SPF_RECORD);
    }

    public function willSPFExceedDNSLimitMC($domainName) {
        if(!$this->spfCountMC){
            $this->setMCSPFCount();
        }
        $domainDNSCount = $this->countDNSLookups($domainName);
        return array($this->spfCountMC + $domainDNSCount > 10, $this->spfCountMC, $domainDNSCount);
    }

    private function getSPFRecord($domainName) {
        $records = dns_get_record($domainName, DNS_TXT);
        if ($records) {
            foreach ($records as $record) {
                if (isset($record["txt"]) && $this->startsWith($record["txt"], "v=spf1")) {
                    return $record["txt"];
                }
            }
        }
        return "";
    }

    private function startsWith($string, $search) {
        return substr($string, 0, strlen($search)) == $search;
    }

    private function countRecursive($domainNames) {
        $lookups = 0;
        foreach ($domainNames as $domainName) {
            $lookups += $this->countDNSLookups($domainName);
        }
        return $lookups;
    }

    private function countMXLookups($mxs) {
        $lookups = 0;
        foreach ($mxs as $mx) {
            $hosts = array();
            $found = getmxrr($mx, $hosts);
            if ($found) {
                $lookups += count($hosts);
            } else {
                // getmxrr() returns a boolean: true, if any records are found; false if no records were found or if an error
                // occurred. this makes it hard to distinguish between an error and no records so we'll just log the
                // error and carry on
                App::getLogger()->warning("found no records or an error during an mx lookup for $mx");
            }
        }
        return $lookups;
    }

    private function getIncludes($record) {
        $includes = array();
        $matches = preg_match_all('/include:([^\s]+)/', $record, $includes);
        if ($matches == 0) {
            return array();
        }
        return $includes[1];
    }

    private function getA($record) {
        $a = array();
        $matches = preg_match_all('/a:([^\s]+)|(a\/\d{1,2}\/\d{1,3}\b)|(a\/\d{1,2}\b)|a\b/', $record, $a);
        if ($matches == 0) {
            return array();
        }
        return $a[1];
    }

    private function getMX($record) {
        $mx = array();
        $matches = preg_match_all('/mx:([^\s]+)|(mx\/\d{1,2}\/\d{1,3}\b)|(mx\/\d{1,2}\b)|mx\b/', $record, $mx);
        if ($matches == 0) {
            return array();
        }
        return $mx[1];
    }

    private function getPtr($record) {
        $ptr = array();
        $matches = preg_match_all('/ptr:([^\s]+)|ptr/', $record, $ptr);
        if ($matches == 0) {
            return array();
        }
        return $ptr[1];
    }

    private function getExists($record) {
        $exists = array();
        $matches = preg_match_all('/exists:([^\s]+)/', $record, $exists);
        if ($matches == 0) {
            return array();
        }
        return $exists[1];
    }

    private function getRedirect($record) {
        $redirect = array();
        $matches = preg_match_all('/redirect=([^\s]+)/', $record, $redirect);
        if ($matches == 0) {
            return array();
        }
        return $redirect[1];
    }
}