From 3c0d1873cea42795d4d421685348480452e52c8a Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Mon, 20 Jul 2020 19:42:00 +0100 Subject: [PATCH 1/9] 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) --- src/Exception/DomainAcceptsNoMail.php | 9 +++ src/Exception/LocalOrReservedDomain.php | 9 +++ src/Validation/DNSCheckValidation.php | 77 ++++++++++++++++--- .../Validation/DNSCheckValidationTest.php | 56 ++++++++++++-- 4 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 src/Exception/DomainAcceptsNoMail.php create mode 100644 src/Exception/LocalOrReservedDomain.php diff --git a/src/Exception/DomainAcceptsNoMail.php b/src/Exception/DomainAcceptsNoMail.php new file mode 100644 index 00000000..40a99705 --- /dev/null +++ b/src/Exception/DomainAcceptsNoMail.php @@ -0,0 +1,9 @@ +error = new LocalOrReservedDomain(); + return false; + } + return $this->checkDNS($host); } @@ -57,21 +89,46 @@ public function getWarnings() 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'); + // Get all MX, A and AAAA DNS records + $dnsRecords = dns_get_record($host, DNS_MX + DNS_A + DNS_AAAA); - if (!$MXresult) { - $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); - $Aresult = checkdnsrr($host, 'A') || checkdnsrr($host, 'AAAA'); - if (!$Aresult) { - $this->error = new NoDNSRecord(); + // No MX, A or AAAA records + if (empty($dnsRecords) || !is_array($dnsRecords)) { + $this->error = new NoDNSRecord(); + return false; + } + + $aRecords = []; + $mxRecords = []; + + // Iterate over all returned DNS records + foreach ($dnsRecords as $dnsRecord) { + if ($dnsRecord['type'] == 'MX') { + // "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; + } + + $mxRecords[] = $dnsRecord; + continue; + } + + if (in_array($dnsRecord['type'], ['A', 'AAAA'])) { + $aRecords[] = $dnsRecord; } } - return $MXresult || $Aresult; + + // No MX record + if (empty($mxRecords)) { + $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); + } + + return true; } -} +} \ No newline at end of file diff --git a/tests/EmailValidator/Validation/DNSCheckValidationTest.php b/tests/EmailValidator/Validation/DNSCheckValidationTest.php index 766dc694..2e45b70c 100644 --- a/tests/EmailValidator/Validation/DNSCheckValidationTest.php +++ b/tests/EmailValidator/Validation/DNSCheckValidationTest.php @@ -4,6 +4,8 @@ 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; @@ -30,34 +32,78 @@ public function validEmailsProvider() ]; } + 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 */ - public function testValidDNS($validEmail) + public function testValidDns($validEmail) { $validation = new DNSCheckValidation(); $this->assertTrue($validation->isValid($validEmail, new EmailLexer())); } - public function testInvalidDNS() + public function testInvalidDns() { $validation = new DNSCheckValidation(); $this->assertFalse($validation->isValid("example@invalid.example.com", new EmailLexer())); } - public function testDNSWarnings() + /** + * @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("example@example.com", new EmailLexer()); + $this->assertEquals($expectedError, $validation->getError()); + $this->assertFalse($isValidResult); + } + + /* + public function testDnsWarnings() { $validation = new DNSCheckValidation(); $expectedWarnings = [NoDNSMXRecord::CODE => new NoDNSMXRecord()]; $validation->isValid("example@invalid.example.com", new EmailLexer()); $this->assertEquals($expectedWarnings, $validation->getWarnings()); } + */ - public function testNoDNSError() + public function testNoDnsError() { $validation = new DNSCheckValidation(); $expectedError = new NoDNSRecord(); $validation->isValid("example@invalid.example.com", new EmailLexer()); $this->assertEquals($expectedError, $validation->getError()); } -} +} \ No newline at end of file From 1e67a37aeb7cde944ee8014c52e281a78d096d9e Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Mon, 20 Jul 2020 23:07:48 +0100 Subject: [PATCH 2/9] 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 --- .../Validation/DNSCheckValidationTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/EmailValidator/Validation/DNSCheckValidationTest.php b/tests/EmailValidator/Validation/DNSCheckValidationTest.php index 2e45b70c..ffde2d3b 100644 --- a/tests/EmailValidator/Validation/DNSCheckValidationTest.php +++ b/tests/EmailValidator/Validation/DNSCheckValidationTest.php @@ -16,16 +16,16 @@ public function validEmailsProvider() { return [ // dot-atom - ['Abc@example.com'], - ['ABC@EXAMPLE.COM'], - ['Abc.123@example.com'], - ['user+mailbox/department=shipping@example.com'], - ['!#$%&\'*+-/=?^_`.{|}~@example.com'], + ['Abc@github.com'], + ['ABC@GITHUB.COM'], + ['Abc.123@github.com'], + ['user+mailbox/department=shipping@github.com'], + ['!#$%&\'*+-/=?^_`.{|}~@github.com'], // quoted string - ['"Abc@def"@example.com'], - ['"Fred\ Bloggs"@example.com'], - ['"Joe.\\Blow"@example.com'], + ['"Abc@def"@github.com'], + ['"Fred\ Bloggs"@github.com'], + ['"Joe.\\Blow"@github.com'], // unicide ['ñandu.cl'], From 02e30711848fd1367080e758210e84ef10da4ef7 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Mon, 20 Jul 2020 23:12:35 +0100 Subject: [PATCH 3/9] Tweak to pass CI psalm issue --- src/Validation/DNSCheckValidation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validation/DNSCheckValidation.php b/src/Validation/DNSCheckValidation.php index c02c9427..3f4e7508 100644 --- a/src/Validation/DNSCheckValidation.php +++ b/src/Validation/DNSCheckValidation.php @@ -98,7 +98,7 @@ protected function checkDNS($host) $dnsRecords = dns_get_record($host, DNS_MX + DNS_A + DNS_AAAA); // No MX, A or AAAA records - if (empty($dnsRecords) || !is_array($dnsRecords)) { + if (empty($dnsRecords)) { $this->error = new NoDNSRecord(); return false; } From 0eb972fdb6aa1cb3b1eb7aa2d2397e44c2d870e2 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Wed, 5 Aug 2020 14:09:40 +0100 Subject: [PATCH 4/9] Encapsulate dns check code. Simplify local and reserved domain checks. Switch test domain --- src/Validation/DNSCheckValidation.php | 77 +++++++++++++------ .../Validation/DNSCheckValidationTest.php | 26 +++---- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/Validation/DNSCheckValidation.php b/src/Validation/DNSCheckValidation.php index 3f4e7508..6c4d3b9b 100644 --- a/src/Validation/DNSCheckValidation.php +++ b/src/Validation/DNSCheckValidation.php @@ -21,6 +21,12 @@ class DNSCheckValidation implements EmailValidation */ private $error; + /** + * @var array + */ + private $mxRecords = []; + + public function __construct() { if (!function_exists('idn_to_ascii')) { @@ -68,7 +74,7 @@ public function isValid($email, EmailLexer $emailLexer) return false; } - return $this->checkDNS($host); + return $this->checkDns($host); } public function getError() @@ -86,49 +92,72 @@ public function getWarnings() * * @return bool */ - protected function checkDNS($host) + protected function checkDns($host) { $variant = INTL_IDNA_VARIANT_2003; if (defined('INTL_IDNA_VARIANT_UTS46')) { $variant = INTL_IDNA_VARIANT_UTS46; } + $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.') . '.'; - // Get all MX, A and AAAA DNS records + return $this->validateDnsRecords($host); + } + + + /** + * Validate the DNS records + * + * @param $dnsRecords 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 records + + // No MX, A or AAAA DNS records if (empty($dnsRecords)) { $this->error = new NoDNSRecord(); return false; } - $aRecords = []; - $mxRecords = []; - - // Iterate over all returned DNS records + // For each DNS record foreach ($dnsRecords as $dnsRecord) { - if ($dnsRecord['type'] == 'MX') { - // "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; - } - - $mxRecords[] = $dnsRecord; - continue; - } - - if (in_array($dnsRecord['type'], ['A', 'AAAA'])) { - $aRecords[] = $dnsRecord; + if (!$this->validateMXRecord($dnsRecord)) { + return false; } } - // No MX record - if (empty($mxRecords)) { + // No MX records (fallback to A or AAAA records) + if (empty($this->mxRecords)) { $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); } return true; } -} \ No newline at end of file + + /** + * Validate an MX record + * + * @param $dnsRecord A DNS record. + */ + 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; + } +} diff --git a/tests/EmailValidator/Validation/DNSCheckValidationTest.php b/tests/EmailValidator/Validation/DNSCheckValidationTest.php index ffde2d3b..5ca24c39 100644 --- a/tests/EmailValidator/Validation/DNSCheckValidationTest.php +++ b/tests/EmailValidator/Validation/DNSCheckValidationTest.php @@ -16,16 +16,16 @@ public function validEmailsProvider() { return [ // dot-atom - ['Abc@github.com'], - ['ABC@GITHUB.COM'], - ['Abc.123@github.com'], - ['user+mailbox/department=shipping@github.com'], - ['!#$%&\'*+-/=?^_`.{|}~@github.com'], + ['Abc@ietf.org'], + ['ABC@ietf.org'], + ['Abc.123@ietf.org'], + ['user+mailbox/department=shipping@ietf.org'], + ['!#$%&\'*+-/=?^_`.{|}~@ietf.org'], // quoted string - ['"Abc@def"@github.com'], - ['"Fred\ Bloggs"@github.com'], - ['"Joe.\\Blow"@github.com'], + ['"Abc@def"@ietf.org'], + ['"Fred\ Bloggs"@ietf.org'], + ['"Joe.\\Blow"@ietf.org'], // unicide ['ñandu.cl'], @@ -57,13 +57,13 @@ public function localOrReservedEmailsProvider() /** * @dataProvider validEmailsProvider */ - public function testValidDns($validEmail) + public function testValidDNS($validEmail) { $validation = new DNSCheckValidation(); $this->assertTrue($validation->isValid($validEmail, new EmailLexer())); } - public function testInvalidDns() + public function testInvalidDNS() { $validation = new DNSCheckValidation(); $this->assertFalse($validation->isValid("example@invalid.example.com", new EmailLexer())); @@ -84,13 +84,13 @@ public function testDomainAcceptsNoMailError() { $validation = new DNSCheckValidation(); $expectedError = new DomainAcceptsNoMail(); - $isValidResult = $validation->isValid("example@example.com", new EmailLexer()); + $isValidResult = $validation->isValid("nullmx@example.com", new EmailLexer()); $this->assertEquals($expectedError, $validation->getError()); $this->assertFalse($isValidResult); } /* - public function testDnsWarnings() + public function testDNSWarnings() { $validation = new DNSCheckValidation(); $expectedWarnings = [NoDNSMXRecord::CODE => new NoDNSMXRecord()]; @@ -99,7 +99,7 @@ public function testDnsWarnings() } */ - public function testNoDnsError() + public function testNoDNSError() { $validation = new DNSCheckValidation(); $expectedError = new NoDNSRecord(); From 6b9e30e0bbdc4494e7732c3c3d42193c85556b2a Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Wed, 5 Aug 2020 14:20:53 +0100 Subject: [PATCH 5/9] Set PHPDoc return type --- src/Validation/DNSCheckValidation.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Validation/DNSCheckValidation.php b/src/Validation/DNSCheckValidation.php index 6c4d3b9b..8e148ce7 100644 --- a/src/Validation/DNSCheckValidation.php +++ b/src/Validation/DNSCheckValidation.php @@ -68,8 +68,11 @@ public function isValid($email, EmailLexer $emailLexer) 'lan', ]; + $isLocalDomain = count($hostParts) <= 1; + $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], $reservedTopLevelDnsNames); + // Exclude reserved top level DNS names - if (count($hostParts) <= 1 || in_array($hostParts[(count($hostParts) - 1)], $reservedTopLevelDnsNames)) { + if ($isLocalDomain || $isReservedTopLevel) { $this->error = new LocalOrReservedDomain(); return false; } @@ -143,6 +146,8 @@ private function validateDnsRecords($host) * Validate an MX record * * @param $dnsRecord A DNS record. + * + * @return bool True if valid. */ private function validateMxRecord($dnsRecord) { From a70fc5f843236879c0ed1a53e92ead4d3b867484 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Wed, 5 Aug 2020 14:23:05 +0100 Subject: [PATCH 6/9] Set PHPDoc return type --- src/Validation/DNSCheckValidation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Validation/DNSCheckValidation.php b/src/Validation/DNSCheckValidation.php index 8e148ce7..29133e5d 100644 --- a/src/Validation/DNSCheckValidation.php +++ b/src/Validation/DNSCheckValidation.php @@ -111,7 +111,7 @@ protected function checkDns($host) /** * Validate the DNS records * - * @param $dnsRecords A set of DNS records in the format returned by dns_get_record. + * @param array $dnsRecords A set of DNS records in the format returned by dns_get_record. * * @return bool True on success. */ @@ -145,7 +145,7 @@ private function validateDnsRecords($host) /** * Validate an MX record * - * @param $dnsRecord A DNS record. + * @param array $dnsRecord A DNS record. * * @return bool True if valid. */ From ee3b9b3bd2618d3c9ffe095fa9a1af89e6124919 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Wed, 5 Aug 2020 14:36:31 +0100 Subject: [PATCH 7/9] Fix Docblock --- src/Validation/DNSCheckValidation.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Validation/DNSCheckValidation.php b/src/Validation/DNSCheckValidation.php index 29133e5d..ad6e21ab 100644 --- a/src/Validation/DNSCheckValidation.php +++ b/src/Validation/DNSCheckValidation.php @@ -109,9 +109,9 @@ protected function checkDns($host) /** - * Validate the DNS records + * Validate the DNS records for given host. * - * @param array $dnsRecords A set of DNS records in the format returned by dns_get_record. + * @param array $host A set of DNS records in the format returned by dns_get_record. * * @return bool True on success. */ @@ -145,7 +145,7 @@ private function validateDnsRecords($host) /** * Validate an MX record * - * @param array $dnsRecord A DNS record. + * @param array $dnsRecord Given DNS record. * * @return bool True if valid. */ From 3fbc3a06338acd3f71ac39887dcfcbc2c919c4a9 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Wed, 5 Aug 2020 14:39:52 +0100 Subject: [PATCH 8/9] Fix Docblock --- src/Validation/DNSCheckValidation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Validation/DNSCheckValidation.php b/src/Validation/DNSCheckValidation.php index ad6e21ab..7026a4da 100644 --- a/src/Validation/DNSCheckValidation.php +++ b/src/Validation/DNSCheckValidation.php @@ -111,7 +111,7 @@ protected function checkDns($host) /** * Validate the DNS records for given host. * - * @param array $host A set of DNS records in the format returned by dns_get_record. + * @param string $host A set of DNS records in the format returned by dns_get_record. * * @return bool True on success. */ From cddaaf33230a4f29582d577ec26b597f261e1422 Mon Sep 17 00:00:00 2001 From: Ben Harris Date: Thu, 6 Aug 2020 11:29:54 +0100 Subject: [PATCH 9/9] Use strict comparisons --- src/Validation/DNSCheckValidation.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Validation/DNSCheckValidation.php b/src/Validation/DNSCheckValidation.php index 7026a4da..a18da94b 100644 --- a/src/Validation/DNSCheckValidation.php +++ b/src/Validation/DNSCheckValidation.php @@ -69,7 +69,7 @@ public function isValid($email, EmailLexer $emailLexer) ]; $isLocalDomain = count($hostParts) <= 1; - $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], $reservedTopLevelDnsNames); + $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], $reservedTopLevelDnsNames, true); // Exclude reserved top level DNS names if ($isLocalDomain || $isReservedTopLevel) { @@ -151,12 +151,12 @@ private function validateDnsRecords($host) */ private function validateMxRecord($dnsRecord) { - if ($dnsRecord['type'] != 'MX') { + 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'] == '.') { + if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') { $this->error = new DomainAcceptsNoMail(); return false; }