Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement DNSCheckValidation #250

Merged
merged 9 commits into from
Aug 8, 2020
9 changes: 9 additions & 0 deletions src/Exception/DomainAcceptsNoMail.php
Original file line number Diff line number Diff line change
@@ -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)';
egulias marked this conversation as resolved.
Show resolved Hide resolved
}
9 changes: 9 additions & 0 deletions src/Exception/LocalOrReservedDomain.php
Original file line number Diff line number Diff line change
@@ -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)';
egulias marked this conversation as resolved.
Show resolved Hide resolved
}
113 changes: 102 additions & 11 deletions src/Validation/DNSCheckValidation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

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;

Expand All @@ -19,6 +21,12 @@ class DNSCheckValidation implements EmailValidation
*/
private $error;

/**
* @var array
*/
private $mxRecords = [];


public function __construct()
{
if (!function_exists('idn_to_ascii')) {
Expand All @@ -36,7 +44,40 @@ public function isValid($email, EmailLexer $emailLexer)
$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()
Expand All @@ -54,24 +95,74 @@ public function getWarnings()
*
* @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) {
BenHarris marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
64 changes: 55 additions & 9 deletions tests/EmailValidator/Validation/DNSCheckValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,22 +16,44 @@ 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@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
*/
Expand All @@ -45,13 +69,35 @@ public function testInvalidDNS()
$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();
$expectedWarnings = [NoDNSMXRecord::CODE => new NoDNSMXRecord()];
$validation->isValid("example@invalid.example.com", new EmailLexer());
$this->assertEquals($expectedWarnings, $validation->getWarnings());
}
*/

public function testNoDNSError()
BenHarris marked this conversation as resolved.
Show resolved Hide resolved
{
Expand All @@ -60,4 +106,4 @@ public function testNoDNSError()
$validation->isValid("example@invalid.example.com", new EmailLexer());
$this->assertEquals($expectedError, $validation->getError());
}
}
}