Compare commits

...

15 Commits

Author SHA1 Message Date
Nicolas Grekas f88dcf4b14 Don't use utf8_en/decode() functions, they're deprecated on PHP 8.2 (#325) 2022-06-18 22:57:19 +02:00
Eduardo Gulias Davis a5ed8d58ed Bug #321 (#323)
* Update dependencies

* dns_get_record wrapper for testing of the function's behaviour

* remove unused classes

* Fix Psalm errors

* Increase PHP Version
2022-05-29 00:19:18 +02:00
Daniel Winzen 312b65911f Fixed Spoofchecking link (#314) 2022-02-18 16:25:29 +01:00
Denis Gurov d51b279b5f Typos (#313)
A few typos in README
2022-02-11 18:09:16 +01:00
Robert Fridzema 9aab2ceed6 Typos (#311) 2022-02-04 21:30:40 +01:00
Julien Gidel 007bfd424e Small contribution - code readability improvements (#306)
* Some code improvements - tests same as beginning

* isUTF8Invalid returns a boolean

* Changes for tests passed

* Revert change + scrutinizer to flaky

* Null coalescing on token to return a null token

* Psalm pass and PHPunit too (php 7.4)

* Minor change to set lookahead to nullToken
2021-11-20 22:34:12 +01:00
Eduardo Gulias Davis 5cc04719d0 CI update to min supported version 2021-10-11 18:18:50 +02:00
Eduardo Gulias Davis c70449e7b1 CI update to supported versions in matrix 2021-10-11 18:15:58 +02:00
Eduardo Gulias Davis 36425ab1f9 Merge branch '3.x' of github.com:egulias/EmailValidator into 3.x 2021-10-11 18:11:58 +02:00
Eduardo Gulias Davis 4c7033b422 CI and dependencies update 2021-10-11 18:11:44 +02:00
Eduardo Gulias Davis 037594add3 Code quality 2021-10-11 17:54:24 +02:00
Eduardo Gulias Davis 4cccb8186b Update badges 2021-10-11 17:52:58 +02:00
Samuel Vicent ee0db30118 MultipleErrors->reason always returns [0] and should return first item from associative array (#305)
* Patch MultipleErrors->reason method by getting always the first item - Added Tests and a custom Exception

* Return EmptyReason instead of throwing exception when MultipleErrors has no reasons (validations accomplished)

Co-authored-by: Samuel Vicent <samuel.vicent@takeachef.com>
2021-10-11 11:18:27 +02:00
John Congdon c81f18a3ef Reset the label when a S_DOT is encountered. (#297)
This will allow multi label domain's to still validate correctly.

Reset the label when a S_DOT is encountered.
2021-04-01 20:37:14 +02:00
Dries Vints 95979e2ad6 Add null return type to getError DocBlock (#293) 2021-03-11 14:36:15 +01:00
27 changed files with 1624 additions and 940 deletions
+3 -2
View File
@@ -3,11 +3,12 @@ imports:
build:
environment:
php: '7.2.13'
php: '7.4'
tests:
override:
-
command: 'vendor/bin/phpunit --coverage-clover=clover.xml --exclude-group slow'
command: 'vendor/bin/phpunit --coverage-clover=clover.xml --exclude-group flaky'
coverage:
file: 'clover.xml'
format: 'clover'
- php-scrutinizer-run
+2 -15
View File
@@ -2,28 +2,15 @@ language: php
matrix:
include:
- php: 7.2
env:
- psalm=yes
dist: bionic
- php: 7.3
dist: bionic
env:
- psalm=yes
- php: 7.4
dist: bionic
env:
- psalm=yes
dist: bionic
- php: 8.0
env:
- psalm=yes
dist: bionic
#ppc64le support code
- php: 7.2
arch: ppc64le
env:
- psalm=yes
dist: bionic
- php: 7.4
arch: ppc64le
env:
@@ -38,7 +25,7 @@ before_script:
- mkdir -p build/logs
script:
- vendor/bin/phpunit --coverage-clover build/logs/clover.xml --exclude-group slow
- vendor/bin/phpunit --coverage-clover build/logs/clover.xml --exclude-group flaky
- if [ "$psalm" = "yes" ]; then vendor/bin/psalm; fi
after_script:
+4 -4
View File
@@ -1,6 +1,6 @@
# Contributing
When contributing to this repository make sure to follow the Pull request process below.
When contributing to this repository make sure to follow the Pull request process below.
Reduce to the minimum 3rd party dependencies.
Please note we have a [code of conduct](#Code of Conduct), please follow it in all your interactions with the project.
@@ -14,7 +14,7 @@ When doing a PR to v2 remember that you also have to do the PR port to v3, or te
3. Describe the changes you are proposing
1. If adding an extra validation state the benefits of adding it and the problem is solving
2. Document in the readme, by adding it to the list
4. Provide appropiate tests for the code you are submitting: aim to keep the existing coverage percentage.
4. Provide appropriate tests for the code you are submitting: aim to keep the existing coverage percentage.
5. Add your Twitter handle (if you have) so we can thank you there.
## License
@@ -139,11 +139,11 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
+7 -8
View File
@@ -1,9 +1,8 @@
# EmailValidator
[![Build Status](https://travis-ci.org/egulias/EmailValidator.svg?branch=3.x)](https://travis-ci.org/egulias/EmailValidator)
[![Build Status](https://app.travis-ci.com/egulias/EmailValidator.svg?branch=3.x)](https://app.travis-ci.com/github/egulias/EmailValidator)
[![Code Quality](https://scrutinizer-ci.com/g/egulias/EmailValidator/badges/quality-score.png?b=3.x)](https://scrutinizer-ci.com/g/egulias/EmailValidator/?branch=3.x)
[![Test Coverage](https://scrutinizer-ci.com/g/egulias/EmailValidator/badges/coverage.png?b=3.x)](https://scrutinizer-ci.com/g/egulias/EmailValidator/?branch=3.x)
[![SymfonyInsight](https://insight.symfony.com/projects/22ba6692-9c02-42e5-a65d-1c5696bfffc6/mini.svg)](https://insight.symfony.com/projects/22ba6692-9c02-42e5-a65d-1c5696bfffc6)
A library for validating emails against several RFC.
@@ -20,7 +19,7 @@ This library aims to support RFCs:
## Supported versions
**Curent major version with full support is v3**
**Current major version with full support is v3**
| Version | Released | EOL | Only critical bug fixes | Full |
| ---- | :----: | :----: | :----: | :----: |
@@ -33,7 +32,7 @@ This library aims to support RFCs:
* PHP 7.2
* [Composer](https://getcomposer.org) is required for installation
* [Spoofchecking](/src/Validation/SpoofCheckValidation.php) and [DNSCheckValidation](/src/Validation/DNSCheckValidation.php) validation requires that your PHP system has the [PHP Internationalization Libraries](https://php.net/manual/en/book.intl.php) (also known as PHP Intl)
* [Spoofchecking](/src/Validation/Extra/SpoofCheckValidation.php) and [DNSCheckValidation](/src/Validation/DNSCheckValidation.php) validation requires that your PHP system has the [PHP Internationalization Libraries](https://php.net/manual/en/book.intl.php) (also known as PHP Intl)
**Note**: `PHP version upgrades will happen to accomodate to the pace of major frameworks. Minor versions bumps will go via minor versions of this library (i.e: PHP7.3 -> v3.x+1). Major versions will go with major versions of the library`
@@ -47,7 +46,7 @@ composer require egulias/email-validator
## Getting Started ##
`EmailValidator`requires you to decide which (or combination of them) validation/s strategy/ies you'd like to follow for each [validation](#available-validations).
`EmailValidator` requires you to decide which (or combination of them) validation/s strategy/ies you'd like to follow for each [validation](#available-validations).
A basic example with the RFC validation
```php
@@ -65,12 +64,12 @@ $validator->isValid("example@example.com", new RFCValidation()); //true
1. [RFCValidation](/src/Validation/RFCValidation.php): Standard RFC-like email validation.
2. [NoRFCWarningsValidation](/src/Validation/NoRFCWarningsValidation.php): RFC-like validation that will fail when warnings* are found.
3. [DNSCheckValidation](/src/Validation/DNSCheckValidation.php): Will check if there are DNS records that signal that the server accepts emails. This does not entails that the email exists.
3. [DNSCheckValidation](/src/Validation/DNSCheckValidation.php): Will check if there are DNS records that signal that the server accepts emails. This does not entail that the email exists.
5. [MultipleValidationWithAnd](/src/Validation/MultipleValidationWithAnd.php): It is a validation that operates over other validations performing a logical and (&&) over the result of each validation.
6. [MessageIDValidation](/src/Validation/MessageIDValidation.php): Follows [RFC2822 for message-id](https://tools.ietf.org/html/rfc2822#section-3.6.4) to validate that field, that has some differences in the domain part.
7. [Your own validation](#how-to-extend): You can extend the library behaviour by implementing your own validations.
*warnings: Warnings are deviations from the RFC that in a broader interpretation are acceptded.
*warnings: Warnings are deviations from the RFC that in a broader interpretation are accepted.
```php
<?php
@@ -85,7 +84,7 @@ $multipleValidations = new MultipleValidationWithAnd([
new RFCValidation(),
new DNSCheckValidation()
]);
//ietf.org has MX records signaling a server with email capabilites
//ietf.org has MX records signaling a server with email capabilities
$validator->isValid("example@ietf.org", $multipleValidations); //true
```
Generated
+1276 -688
View File
File diff suppressed because it is too large Load Diff
+16 -30
View File
@@ -1,31 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
executionOrder="defects"
processIsolation="false"
stopOnFailure="false"
>
<testsuites>
<testsuite name="EmailValidator Test Suite">
<directory>./tests/EmailValidator</directory>
<exclude>./vendor/</exclude>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
<exclude>
<directory suffix=".php">./src/Result/Reason</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" executionOrder="defects" processIsolation="false" stopOnFailure="false">
<coverage>
<include>
<directory>./src/</directory>
</include>
<exclude>
<directory suffix=".php">./src/Result/Reason</directory>
</exclude>
</coverage>
<testsuites>
<testsuite name="EmailValidator Test Suite">
<directory>./tests/EmailValidator</directory>
<exclude>./vendor/</exclude>
</testsuite>
</testsuites>
</phpunit>
-1
View File
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
<psalm
totallyTyped="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config ./vendor/vimeo/psalm/config.xsd"
+78 -100
View File
@@ -61,7 +61,7 @@ class EmailLexer extends AbstractLexer
*
* @var array
*/
protected $charValue = array(
protected $charValue = [
'{' => self::S_OPENCURLYBRACES,
'}' => self::S_CLOSECURLYBRACES,
'(' => self::S_OPENPARENTHESIS,
@@ -105,11 +105,29 @@ class EmailLexer extends AbstractLexer
'?' => self::QUESTIONMARK,
'#' => self::NUMBER_SIGN,
'¡' => self::INVERT_EXCLAMATION,
);
];
/**
* @var bool
*/
const INVALID_CHARS_REGEX = "/[^\p{S}\p{C}\p{Cc}]+/iu";
const VALID_UTF8_REGEX = '/\p{Cc}+/u';
const CATCHABLE_PATTERNS = [
'[a-zA-Z]+[46]?', //ASCII and domain literal
'[^\x00-\x7F]', //UTF-8
'[0-9]+',
'\r\n',
'::',
'\s+?',
'.',
];
const NON_CATCHABLE_PATTERNS = [
'[\xA0-\xff]+',
];
const MODIFIERS = 'iu';
/** @var bool */
protected $hasInvalidTokens = false;
/**
@@ -133,27 +151,21 @@ class EmailLexer extends AbstractLexer
/**
* The next token in the input.
*
* @var array|null
* @var array{position: int, type: int|null|string, value: int|string}|null
*/
public $lookahead;
/**
* @psalm-var array{value:'', type:null, position:0}
*/
/** @psalm-var array{value:'', type:null, position:0} */
private static $nullToken = [
'value' => '',
'type' => null,
'position' => 0,
];
/**
* @var string
*/
/** @var string */
private $accumulator = '';
/**
* @var bool
*/
/** @var bool */
private $hasToRecord = false;
public function __construct()
@@ -162,24 +174,13 @@ class EmailLexer extends AbstractLexer
$this->lookahead = null;
}
/**
* @return void
*/
public function reset()
public function reset() : void
{
$this->hasInvalidTokens = false;
parent::reset();
$this->previous = $this->token = self::$nullToken;
}
/**
* @return bool
*/
public function hasInvalidTokens()
{
return $this->hasInvalidTokens;
}
/**
* @param int $type
* @throws \UnexpectedValueException
@@ -187,7 +188,7 @@ class EmailLexer extends AbstractLexer
*
* @psalm-suppress InvalidScalarArgument
*/
public function find($type)
public function find($type) : bool
{
$search = clone $this;
$search->skipUntil($type);
@@ -198,30 +199,24 @@ class EmailLexer extends AbstractLexer
return true;
}
/**
* getPrevious
*
* @return array
*/
public function getPrevious()
{
return $this->previous;
}
/**
* moveNext
*
* @return boolean
*/
public function moveNext()
public function moveNext() : bool
{
if ($this->hasToRecord && $this->previous === self::$nullToken) {
$this->accumulator .= $this->token['value'];
}
$this->previous = $this->token;
if($this->lookahead === null) {
$this->lookahead = self::$nullToken;
}
$hasNext = parent::moveNext();
$this->token = $this->token ?: self::$nullToken;
if ($this->hasToRecord) {
$this->accumulator .= $this->token['value'];
@@ -230,36 +225,6 @@ class EmailLexer extends AbstractLexer
return $hasNext;
}
/**
* Lexical catchable patterns.
*
* @return string[]
*/
protected function getCatchablePatterns()
{
return array(
'[a-zA-Z]+[46]?', //ASCII and domain literal
'[^\x00-\x7F]', //UTF-8
'[0-9]+',
'\r\n',
'::',
'\s+?',
'.',
);
}
/**
* Lexical non-catchable patterns.
*
* @return string[]
*/
protected function getNonCatchablePatterns()
{
return [
'[\xA0-\xff]+',
];
}
/**
* Retrieve token type. Also processes the token value if necessary.
*
@@ -272,7 +237,7 @@ class EmailLexer extends AbstractLexer
$encoded = $value;
if (mb_detect_encoding($value, 'auto', true) !== 'UTF-8') {
$encoded = utf8_encode($value);
$encoded = mb_convert_encoding($value, 'UTF-8', 'Windows-1252');
}
if ($this->isValid($encoded)) {
@@ -292,51 +257,64 @@ class EmailLexer extends AbstractLexer
return self::GENERIC;
}
protected function isInvalidChar(string $value) : bool
{
if(preg_match("/[^\p{S}\p{C}\p{Cc}]+/iu", $value) ) {
return false;
}
return true;
}
protected function isValid(string $value) : bool
{
if (isset($this->charValue[$value])) {
return true;
}
return false;
return isset($this->charValue[$value]);
}
/**
* @param string $value
* @return bool
*/
protected function isNullType($value)
protected function isNullType(string $value) : bool
{
if ($value === "\0") {
return true;
}
return $value === "\0";
}
return false;
protected function isInvalidChar(string $value) : bool
{
return !preg_match(self::INVALID_CHARS_REGEX, $value);
}
protected function isUTF8Invalid(string $value) : bool
{
if (preg_match('/\p{Cc}+/u', $value)) {
return true;
}
return preg_match(self::VALID_UTF8_REGEX, $value) !== false;
}
return false;
public function hasInvalidTokens() : bool
{
return $this->hasInvalidTokens;
}
/**
* @return string
* getPrevious
*
* @return array
*/
protected function getModifiers()
public function getPrevious() : array
{
return 'iu';
return $this->previous;
}
/**
* Lexical catchable patterns.
*
* @return string[]
*/
protected function getCatchablePatterns() : array
{
return self::CATCHABLE_PATTERNS;
}
/**
* Lexical non-catchable patterns.
*
* @return string[]
*/
protected function getNonCatchablePatterns() : array
{
return self::NON_CATCHABLE_PATTERNS;
}
protected function getModifiers() : string
{
return self::MODIFIERS;
}
public function getAccumulatedValues() : string
-1
View File
@@ -3,7 +3,6 @@
namespace Egulias\EmailValidator;
use Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Parser\IDLeftPart;
use Egulias\EmailValidator\Parser\IDRightPart;
+15 -16
View File
@@ -22,6 +22,15 @@ use Egulias\EmailValidator\Warning\DomainLiteral as WarningDomainLiteral;
class DomainLiteral extends PartParser
{
const IPV4_REGEX = '/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
const OBSOLETE_WARNINGS = [
EmailLexer::INVALID,
EmailLexer::C_DEL,
EmailLexer::S_LF,
EmailLexer::S_BACKSLASH
];
public function parse() : Result
{
$this->addTagWarnings();
@@ -41,7 +50,7 @@ class DomainLiteral extends PartParser
}
if ($this->lexer->isNextTokenAny(
array(EmailLexer::S_HTAB, EmailLexer::S_SP, $this->lexer->token['type'] === EmailLexer::CRLF)
array(EmailLexer::S_HTAB, EmailLexer::S_SP, EmailLexer::CRLF)
)) {
$this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS();
$this->parseFWS();
@@ -138,11 +147,8 @@ class DomainLiteral extends PartParser
public function convertIPv4ToIPv6(string $addressLiteralIPv4) : string
{
$matchesIP = array();
$IPv4Match = preg_match(
'/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/',
$addressLiteralIPv4,
$matchesIP);
$matchesIP = [];
$IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteralIPv4, $matchesIP);
// Extract IPv4 part from the end of the address-literal (if there is one)
if ($IPv4Match > 0) {
@@ -164,11 +170,8 @@ class DomainLiteral extends PartParser
*/
protected function checkIPV4Tag($addressLiteral) : bool
{
$matchesIP = array();
$IPv4Match = preg_match(
'/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/',
$addressLiteral,
$matchesIP);
$matchesIP = [];
$IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteral, $matchesIP);
// Extract IPv4 part from the end of the address-literal (if there is one)
@@ -186,11 +189,7 @@ class DomainLiteral extends PartParser
private function addObsoleteWarnings() : void
{
if ($this->lexer->token['type'] === EmailLexer::INVALID ||
$this->lexer->token['type'] === EmailLexer::C_DEL ||
$this->lexer->token['type'] === EmailLexer::S_LF ||
$this->lexer->token['type'] === EmailLexer::S_BACKSLASH
) {
if(in_array($this->lexer->token['type'], self::OBSOLETE_WARNINGS)) {
$this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT();
}
}
+3 -3
View File
@@ -280,9 +280,9 @@ class DomainPart extends PartParser
{
if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) {
if ($this->isLabelTooLong($this->label)) {
$this->label = '';
return new InvalidEmail(new LabelTooLong(), $this->lexer->token['value']);
}
$this->label = '';
}
$this->label .= $this->lexer->token['value'];
return new ValidEmail();
@@ -292,7 +292,7 @@ class DomainPart extends PartParser
private function isLabelTooLong(string $label) : bool
{
if (preg_match('/[^\x00-\x7F]/', $label)) {
idn_to_ascii(utf8_decode($label), IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo);
return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG);
}
return strlen($label) > self::LABEL_MAX_LENGTH;
@@ -309,4 +309,4 @@ class DomainPart extends PartParser
{
return $this->domainPart;
}
}
}
+5 -4
View File
@@ -19,18 +19,19 @@ class DoubleQuote extends PartParser
$validQuotedString = $this->checkDQUOTE();
if($validQuotedString->isInvalid()) return $validQuotedString;
$special = array(
$special = [
EmailLexer::S_CR => true,
EmailLexer::S_HTAB => true,
EmailLexer::S_LF => true
);
];
$invalid = array(
$invalid = [
EmailLexer::C_NUL => true,
EmailLexer::S_HTAB => true,
EmailLexer::S_CR => true,
EmailLexer::S_LF => true
);
];
$setSpecialsWarning = true;
$this->lexer->moveNext();
+9 -5
View File
@@ -15,6 +15,14 @@ use Egulias\EmailValidator\Result\ValidEmail;
class FoldingWhiteSpace extends PartParser
{
const FWS_TYPES = [
EmailLexer::S_SP,
EmailLexer::S_HTAB,
EmailLexer::S_CR,
EmailLexer::S_LF,
EmailLexer::CRLF
];
public function parse() : Result
{
if (!$this->isFWS()) {
@@ -73,10 +81,6 @@ class FoldingWhiteSpace extends PartParser
return false;
}
return $this->lexer->token['type'] === EmailLexer::S_SP ||
$this->lexer->token['type'] === EmailLexer::S_HTAB ||
$this->lexer->token['type'] === EmailLexer::S_CR ||
$this->lexer->token['type'] === EmailLexer::S_LF ||
$this->lexer->token['type'] === EmailLexer::CRLF;
return in_array($this->lexer->token['type'], self::FWS_TYPES);
}
}
+2 -2
View File
@@ -12,14 +12,14 @@ class IDRightPart extends DomainPart
{
protected function validateTokens(bool $hasComments) : Result
{
$invalidDomainTokens = array(
$invalidDomainTokens = [
EmailLexer::S_DQUOTE => true,
EmailLexer::S_SQUOTE => true,
EmailLexer::S_BACKTICK => true,
EmailLexer::S_SEMICOLON => true,
EmailLexer::S_GREATERTHAN => true,
EmailLexer::S_LOWERTHAN => true,
);
];
if (isset($invalidDomainTokens[$this->lexer->token['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']);
+12 -11
View File
@@ -15,6 +15,17 @@ use Egulias\EmailValidator\Parser\CommentStrategy\LocalComment;
class LocalPart extends PartParser
{
const INVALID_TOKENS = [
EmailLexer::S_COMMA => EmailLexer::S_COMMA,
EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET,
EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET,
EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN,
EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN,
EmailLexer::S_COLON => EmailLexer::S_COLON,
EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON,
EmailLexer::INVALID => EmailLexer::INVALID
];
/**
* @var string
*/
@@ -88,17 +99,7 @@ class LocalPart extends PartParser
protected function validateTokens(bool $hasComments) : Result
{
$invalidTokens = array(
EmailLexer::S_COMMA => EmailLexer::S_COMMA,
EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET,
EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET,
EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN,
EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN,
EmailLexer::S_COLON => EmailLexer::S_COLON,
EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON,
EmailLexer::INVALID => EmailLexer::INVALID
);
if (isset($invalidTokens[$this->lexer->token['type']])) {
if (isset(self::INVALID_TOKENS[$this->lexer->token['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->token['value']);
}
return new ValidEmail();
+4 -2
View File
@@ -2,7 +2,7 @@
namespace Egulias\EmailValidator\Result;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\EmptyReason;
use Egulias\EmailValidator\Result\Reason\Reason;
/**
@@ -34,7 +34,9 @@ class MultipleErrors extends InvalidEmail
public function reason() : Reason
{
return $this->reasons[0];
return 0 !== count($this->reasons)
? current($this->reasons)
: new EmptyReason();
}
public function description() : string
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class EmptyReason implements Reason
{
public function code() : int
{
return 0;
}
public function description() : string
{
return 'Empty reason';
}
}
+1 -1
View File
@@ -11,6 +11,6 @@ class UnOpenedComment implements Reason
public function description(): string
{
return 'Missing openning comment parentheses - https://tools.ietf.org/html/rfc5322#section-3.2.2';
return 'Missing opening comment parentheses - https://tools.ietf.org/html/rfc5322#section-3.2.2';
}
}
+46 -38
View File
@@ -2,6 +2,7 @@
namespace Egulias\EmailValidator\Validation;
use Egulias\EmailValidator\Validation\DNSGetRecordWrapper;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\DomainAcceptsNoMail;
@@ -17,6 +18,29 @@ class DNSCheckValidation implements EmailValidation
*/
protected const DNS_RECORD_TYPES_TO_CHECK = DNS_MX + DNS_A + DNS_AAAA;
/**
* 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)
*/
const RESERVED_DNS_TOP_LEVEL_NAMES = [
// Reserved Top Level DNS Names
'test',
'example',
'invalid',
'localhost',
// mDNS
'local',
// Private DNS Namespaces
'intranet',
'internal',
'private',
'corp',
'home',
'lan',
];
/**
* @var array
*/
@@ -32,12 +56,22 @@ class DNSCheckValidation implements EmailValidation
*/
private $mxRecords = [];
/**
* @var DNSGetRecordWrapper
*/
private $dnsGetRecord;
public function __construct()
public function __construct(DNSGetRecordWrapper $dnsGetRecord = null)
{
if (!function_exists('idn_to_ascii')) {
throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__));
}
if ($dnsGetRecord == null) {
$dnsGetRecord = new DNSGetRecordWrapper();
}
$this->dnsGetRecord = $dnsGetRecord;
}
public function isValid(string $email, EmailLexer $emailLexer) : bool
@@ -53,29 +87,8 @@ class DNSCheckValidation implements EmailValidation
// 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);
$isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], self::RESERVED_DNS_TOP_LEVEL_NAMES, true);
// Exclude reserved top level DNS names
if ($isLocalDomain || $isReservedTopLevel) {
@@ -120,27 +133,17 @@ class DNSCheckValidation implements EmailValidation
*/
private function validateDnsRecords($host) : bool
{
// A workaround to fix https://bugs.php.net/bug.php?id=73149
/** @psalm-suppress InvalidArgument */
set_error_handler(
static function (int $errorLevel, string $errorMessage): ?bool {
throw new \RuntimeException("Unable to get DNS record for the host: $errorMessage");
}
);
$dnsRecordsResult = $this->dnsGetRecord->getRecords($host, static::DNS_RECORD_TYPES_TO_CHECK);
try {
// Get all MX, A and AAAA DNS records for host
$dnsRecords = dns_get_record($host, static::DNS_RECORD_TYPES_TO_CHECK);
} catch (\RuntimeException $exception) {
if ($dnsRecordsResult->withError()) {
$this->error = new InvalidEmail(new UnableToGetDNSRecord(), '');
return false;
} finally {
restore_error_handler();
}
$dnsRecords = $dnsRecordsResult->getRecords();
// No MX, A or AAAA DNS records
if ($dnsRecords === [] || $dnsRecords === false) {
if ($dnsRecords === []) {
$this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
return false;
}
@@ -167,6 +170,11 @@ class DNSCheckValidation implements EmailValidation
*/
private function validateMxRecord($dnsRecord) : bool
{
if (!isset($dnsRecord['type'])) {
$this->error = new InvalidEmail(new ReasonNoDNSRecord(), '');
return false;
}
if ($dnsRecord['type'] !== 'MX') {
return true;
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace Egulias\EmailValidator\Validation;
class DNSGetRecordWrapper
{
/**
* @param string $host
* @param int $type
*/
public function getRecords(string $host, int $type) : DNSRecords
{
// A workaround to fix https://bugs.php.net/bug.php?id=73149
/** @psalm-suppress InvalidArgument */
set_error_handler(
static function (int $errorLevel, string $errorMessage): ?bool {
throw new \RuntimeException("Unable to get DNS record for the host: $errorMessage");
}
);
try {
// Get all MX, A and AAAA DNS records for host
return new DNSRecords(dns_get_record($host, $type));
} catch (\RuntimeException $exception) {
return new DNSRecords([], true);
} finally {
restore_error_handler();
}
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace Egulias\EmailValidator\Validation;
class DNSRecords
{
/**
* @var array $records
*/
private $records = [];
/**
* @var bool $error
*/
private $error = false;
public function __construct(array $records, bool $error = false)
{
$this->records = $records;
$this->error = $error;
}
public function getRecords() : array
{
return $this->records;
}
public function withError() : bool
{
return $this->error;
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ interface EmailValidation
/**
* Returns the validation error.
*
* @return InvalidEmail
* @return InvalidEmail|null
*/
public function getError() : ?InvalidEmail;
+1 -1
View File
@@ -10,7 +10,7 @@ use Egulias\EmailValidator\Result\MultipleErrors;
class MultipleValidationWithAnd implements EmailValidation
{
/**
* If one of validations fails, the remaining validations will be skept.
* If one of validations fails, the remaining validations will be skipped.
* This means MultipleErrors will only contain a single error, the first found.
*/
const STOP_ON_ERROR = 0;
+1 -1
View File
@@ -42,6 +42,6 @@ abstract class Warning
public function __toString()
{
return $this->message() . " rfc: " . $this->rfcNumber . "interal code: " . static::CODE;
return $this->message() . " rfc: " . $this->rfcNumber . "internal code: " . static::CODE;
}
}
@@ -2,10 +2,11 @@
namespace Egulias\EmailValidator\Tests\EmailValidator\Reason;
use PHPUnit\Framework\TestCase;
use Egulias\EmailValidator\Result\MultipleErrors;
use Egulias\EmailValidator\Result\Reason\EmptyReason;
use Egulias\EmailValidator\Tests\EmailValidator\Dummy\AnotherDummyReason;
use Egulias\EmailValidator\Tests\EmailValidator\Dummy\DummyReason;
use PHPUnit\Framework\TestCase;
class MultipleErrorsTest extends TestCase
{
@@ -35,4 +36,31 @@ class MultipleErrorsTest extends TestCase
$this->assertEquals($expectedReason, $multiError->description());
$this->assertEquals($error1, $multiError->reason());
}
public function testRetrieveFirstReasonWithReasonCodeEqualsZero(): void
{
$error1 = new DummyReason();
$multiError = new MultipleErrors();
$multiError->addReason($error1);
$this->assertEquals($error1, $multiError->reason());
}
public function testRetrieveFirstReasonWithReasonCodeDistinctToZero(): void
{
$error1 = new AnotherDummyReason();
$multiError = new MultipleErrors();
$multiError->addReason($error1);
$this->assertEquals($error1, $multiError->reason());
}
public function testRetrieveFirstReasonWithNoReasonAdded()
{
$emptyReason = new EmptyReason();
$multiError = new MultipleErrors();
$this->assertEquals($emptyReason, $multiError->reason());
}
}
@@ -9,6 +9,8 @@ use Egulias\EmailValidator\Result\Reason\LocalOrReservedDomain;
use Egulias\EmailValidator\Result\Reason\NoDNSRecord;
use Egulias\EmailValidator\Result\Reason\UnableToGetDNSRecord;
use Egulias\EmailValidator\Validation\DNSCheckValidation;
use Egulias\EmailValidator\Validation\DNSGetRecordWrapper;
use Egulias\EmailValidator\Validation\DNSRecords;
use Egulias\EmailValidator\Warning\NoDNSMXRecord;
use PHPUnit\Framework\TestCase;
@@ -109,20 +111,42 @@ class DNSCheckValidationTest extends TestCase
}
/**
* @group slow
* @group flaky
*/
public function testUnableToGetDNSRecord()
{
error_reporting(\E_ALL);
// UnableToGetDNSRecord raises on network errors (e.g. timeout) that we cant emulate in tests (for sure),
// but we can try to get timeout error by trying to fetch all DNS records
$validation = new class extends DNSCheckValidation {
protected const DNS_RECORD_TYPES_TO_CHECK = \DNS_ALL;
// but we can simulate with the wrapper helper
$wrapper = new class extends DNSGetRecordWrapper {
public function getRecords(string $host, int $type) : DNSRecords
{
return new DNSRecords([], true);
}
};
$validation = new DNSCheckValidation($wrapper);
$expectedError = new InvalidEmail(new UnableToGetDNSRecord(), '');
$validation->isValid('example@invalid.example.com', new EmailLexer());
$this->assertEquals($expectedError, $validation->getError());
}
}
public function testMissingTypeKey()
{
$wrapper = new class extends DNSGetRecordWrapper {
public function getRecords(string $host, int $type): DNSRecords
{
return new DNSRecords(['host' => 'test']);
}
};
$validation = new DNSCheckValidation($wrapper);
$expectedError = new InvalidEmail(new NoDNSRecord(), '');
$validation->isValid('example@invalid.example.com', new EmailLexer());
$this->assertEquals($expectedError, $validation->getError());
}
}
@@ -76,6 +76,7 @@ class RFCValidationDomainPartTest extends TestCase
['validipv4@[127.0.0.0]'],
['validipv4@127.0.0.0'],
['withhyphen@domain-exam.com'],
['valid_long_domain@71846jnrsoj91yfhc18rkbrf90ue3onl8y46js38kae8inz0t1.5a-xdycuau.na49.le.example.com']
);
}