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];
}
}