mirror of
https://github.com/egulias/EmailValidator.git
synced 2026-08-30 20:18:04 +00:00
Reimplement DNSCheckValidation (#250)
* Reimplement DNSCheckValidation to utilise get_dns_record and extend support for detection of Null MX records (RFC7505) and reserved, mDNS and private namespaces (RFC2606 & RFC6762) * Change valid email tests to use github.com instead of example.com as example.com now (correctly) fails the tests due to the Null MX case * Tweak to pass CI psalm issue * Encapsulate dns check code. Simplify local and reserved domain checks. Switch test domain * Set PHPDoc return type * Use strict comparisons
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Egulias\EmailValidator\Exception;
|
||||
|
||||
class DomainAcceptsNoMail extends InvalidEmail
|
||||
{
|
||||
const CODE = 154;
|
||||
const REASON = 'Domain accepts no mail (Null MX, RFC7505)';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Egulias\EmailValidator\Exception;
|
||||
|
||||
class LocalOrReservedDomain extends InvalidEmail
|
||||
{
|
||||
const CODE = 153;
|
||||
const REASON = 'Local, mDNS or reserved domain (RFC2606, RFC6762)';
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace Egulias\EmailValidator\Validation;
|
||||
|
||||
use Egulias\EmailValidator\EmailLexer;
|
||||
use Egulias\EmailValidator\Exception\InvalidEmail;
|
||||
use Egulias\EmailValidator\Exception\LocalOrReservedDomain;
|
||||
use Egulias\EmailValidator\Exception\DomainAcceptsNoMail;
|
||||
use Egulias\EmailValidator\Warning\NoDNSMXRecord;
|
||||
use Egulias\EmailValidator\Exception\NoDNSRecord;
|
||||
|
||||
@@ -19,6 +21,12 @@ class DNSCheckValidation implements EmailValidation
|
||||
*/
|
||||
private $error;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $mxRecords = [];
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!function_exists('idn_to_ascii')) {
|
||||
@@ -36,7 +44,40 @@ class DNSCheckValidation implements EmailValidation
|
||||
$host = substr($email, $lastAtPos + 1);
|
||||
}
|
||||
|
||||
return $this->checkDNS($host);
|
||||
// Get the domain parts
|
||||
$hostParts = explode('.', $host);
|
||||
|
||||
// Reserved Top Level DNS Names (https://tools.ietf.org/html/rfc2606#section-2),
|
||||
// mDNS and private DNS Namespaces (https://tools.ietf.org/html/rfc6762#appendix-G)
|
||||
$reservedTopLevelDnsNames = [
|
||||
// Reserved Top Level DNS Names
|
||||
'test',
|
||||
'example',
|
||||
'invalid',
|
||||
'localhost',
|
||||
|
||||
// mDNS
|
||||
'local',
|
||||
|
||||
// Private DNS Namespaces
|
||||
'intranet',
|
||||
'internal',
|
||||
'private',
|
||||
'corp',
|
||||
'home',
|
||||
'lan',
|
||||
];
|
||||
|
||||
$isLocalDomain = count($hostParts) <= 1;
|
||||
$isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], $reservedTopLevelDnsNames, true);
|
||||
|
||||
// Exclude reserved top level DNS names
|
||||
if ($isLocalDomain || $isReservedTopLevel) {
|
||||
$this->error = new LocalOrReservedDomain();
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->checkDns($host);
|
||||
}
|
||||
|
||||
public function getError()
|
||||
@@ -54,24 +95,74 @@ class DNSCheckValidation implements EmailValidation
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkDNS($host)
|
||||
protected function checkDns($host)
|
||||
{
|
||||
$variant = INTL_IDNA_VARIANT_2003;
|
||||
if ( defined('INTL_IDNA_VARIANT_UTS46') ) {
|
||||
if (defined('INTL_IDNA_VARIANT_UTS46')) {
|
||||
$variant = INTL_IDNA_VARIANT_UTS46;
|
||||
}
|
||||
|
||||
$host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.') . '.';
|
||||
|
||||
$Aresult = true;
|
||||
$MXresult = checkdnsrr($host, 'MX');
|
||||
return $this->validateDnsRecords($host);
|
||||
}
|
||||
|
||||
if (!$MXresult) {
|
||||
$this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord();
|
||||
$Aresult = checkdnsrr($host, 'A') || checkdnsrr($host, 'AAAA');
|
||||
if (!$Aresult) {
|
||||
$this->error = new NoDNSRecord();
|
||||
|
||||
/**
|
||||
* Validate the DNS records for given host.
|
||||
*
|
||||
* @param string $host A set of DNS records in the format returned by dns_get_record.
|
||||
*
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function validateDnsRecords($host)
|
||||
{
|
||||
// Get all MX, A and AAAA DNS records for host
|
||||
$dnsRecords = dns_get_record($host, DNS_MX + DNS_A + DNS_AAAA);
|
||||
|
||||
|
||||
// No MX, A or AAAA DNS records
|
||||
if (empty($dnsRecords)) {
|
||||
$this->error = new NoDNSRecord();
|
||||
return false;
|
||||
}
|
||||
|
||||
// For each DNS record
|
||||
foreach ($dnsRecords as $dnsRecord) {
|
||||
if (!$this->validateMXRecord($dnsRecord)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $MXresult || $Aresult;
|
||||
|
||||
// No MX records (fallback to A or AAAA records)
|
||||
if (empty($this->mxRecords)) {
|
||||
$this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an MX record
|
||||
*
|
||||
* @param array $dnsRecord Given DNS record.
|
||||
*
|
||||
* @return bool True if valid.
|
||||
*/
|
||||
private function validateMxRecord($dnsRecord)
|
||||
{
|
||||
if ($dnsRecord['type'] !== 'MX') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505)
|
||||
if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') {
|
||||
$this->error = new DomainAcceptsNoMail();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->mxRecords[] = $dnsRecord;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace Egulias\EmailValidator\Tests\EmailValidator\Validation;
|
||||
|
||||
use Egulias\EmailValidator\EmailLexer;
|
||||
use Egulias\EmailValidator\Exception\NoDNSRecord;
|
||||
use Egulias\EmailValidator\Exception\LocalOrReservedDomain;
|
||||
use Egulias\EmailValidator\Exception\DomainAcceptsNoMail;
|
||||
use Egulias\EmailValidator\Validation\DNSCheckValidation;
|
||||
use Egulias\EmailValidator\Warning\NoDNSMXRecord;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -14,22 +16,44 @@ class DNSCheckValidationTest extends TestCase
|
||||
{
|
||||
return [
|
||||
// dot-atom
|
||||
['Abc@example.com'],
|
||||
['ABC@EXAMPLE.COM'],
|
||||
['Abc.123@example.com'],
|
||||
['user+mailbox/department=shipping@example.com'],
|
||||
['!#$%&\'*+-/=?^_`.{|}~@example.com'],
|
||||
['Abc@ietf.org'],
|
||||
['ABC@ietf.org'],
|
||||
['Abc.123@ietf.org'],
|
||||
['user+mailbox/department=shipping@ietf.org'],
|
||||
['!#$%&\'*+-/=?^_`.{|}~@ietf.org'],
|
||||
|
||||
// quoted string
|
||||
['"Abc@def"@example.com'],
|
||||
['"Fred\ Bloggs"@example.com'],
|
||||
['"Joe.\\Blow"@example.com'],
|
||||
['"Abc@def"@ietf.org'],
|
||||
['"Fred\ Bloggs"@ietf.org'],
|
||||
['"Joe.\\Blow"@ietf.org'],
|
||||
|
||||
// unicide
|
||||
['ñandu.cl'],
|
||||
];
|
||||
}
|
||||
|
||||
public function localOrReservedEmailsProvider()
|
||||
{
|
||||
return [
|
||||
// Reserved Top Level DNS Names
|
||||
['test'],
|
||||
['example'],
|
||||
['invalid'],
|
||||
['localhost'],
|
||||
|
||||
// mDNS
|
||||
['local'],
|
||||
|
||||
// Private DNS Namespaces
|
||||
['intranet'],
|
||||
['internal'],
|
||||
['private'],
|
||||
['corp'],
|
||||
['home'],
|
||||
['lan'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider validEmailsProvider
|
||||
*/
|
||||
@@ -45,6 +69,27 @@ class DNSCheckValidationTest extends TestCase
|
||||
$this->assertFalse($validation->isValid("example@invalid.example.com", new EmailLexer()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider localOrReservedEmailsProvider
|
||||
*/
|
||||
public function testLocalOrReservedDomainError($localOrReservedEmails)
|
||||
{
|
||||
$validation = new DNSCheckValidation();
|
||||
$expectedError = new LocalOrReservedDomain();
|
||||
$validation->isValid($localOrReservedEmails, new EmailLexer());
|
||||
$this->assertEquals($expectedError, $validation->getError());
|
||||
}
|
||||
|
||||
public function testDomainAcceptsNoMailError()
|
||||
{
|
||||
$validation = new DNSCheckValidation();
|
||||
$expectedError = new DomainAcceptsNoMail();
|
||||
$isValidResult = $validation->isValid("nullmx@example.com", new EmailLexer());
|
||||
$this->assertEquals($expectedError, $validation->getError());
|
||||
$this->assertFalse($isValidResult);
|
||||
}
|
||||
|
||||
/*
|
||||
public function testDNSWarnings()
|
||||
{
|
||||
$validation = new DNSCheckValidation();
|
||||
@@ -52,6 +97,7 @@ class DNSCheckValidationTest extends TestCase
|
||||
$validation->isValid("example@invalid.example.com", new EmailLexer());
|
||||
$this->assertEquals($expectedWarnings, $validation->getWarnings());
|
||||
}
|
||||
*/
|
||||
|
||||
public function testNoDNSError()
|
||||
{
|
||||
@@ -60,4 +106,4 @@ class DNSCheckValidationTest extends TestCase
|
||||
$validation->isValid("example@invalid.example.com", new EmailLexer());
|
||||
$this->assertEquals($expectedError, $validation->getError());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user