Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)';
}
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)';
}
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) {
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("[email protected]", 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("[email protected]", new EmailLexer());
$this->assertEquals($expectedError, $validation->getError());
$this->assertFalse($isValidResult);
}

/*
public function testDNSWarnings()
{
$validation = new DNSCheckValidation();
$expectedWarnings = [NoDNSMXRecord::CODE => new NoDNSMXRecord()];
$validation->isValid("[email protected]", new EmailLexer());
$this->assertEquals($expectedWarnings, $validation->getWarnings());
}
*/

public function testNoDNSError()
{
Expand All @@ -60,4 +106,4 @@ public function testNoDNSError()
$validation->isValid("[email protected]", new EmailLexer());
$this->assertEquals($expectedError, $validation->getError());
}
}
}