mirror of
https://github.com/egulias/EmailValidator.git
synced 2026-08-31 20:50:01 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f88dcf4b14 | |||
| a5ed8d58ed | |||
| 312b65911f | |||
| d51b279b5f | |||
| 9aab2ceed6 | |||
| 007bfd424e | |||
| 5cc04719d0 | |||
| c70449e7b1 | |||
| 36425ab1f9 | |||
| 4c7033b422 | |||
| 037594add3 | |||
| 4cccb8186b | |||
| ee0db30118 | |||
| c81f18a3ef | |||
| 95979e2ad6 |
+3
-2
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# EmailValidator
|
||||
|
||||
[](https://travis-ci.org/egulias/EmailValidator)
|
||||
[](https://app.travis-ci.com/github/egulias/EmailValidator)
|
||||
[](https://scrutinizer-ci.com/g/egulias/EmailValidator/?branch=3.x)
|
||||
[](https://scrutinizer-ci.com/g/egulias/EmailValidator/?branch=3.x)
|
||||
[](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
File diff suppressed because it is too large
Load Diff
+16
-30
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ interface EmailValidation
|
||||
/**
|
||||
* Returns the validation error.
|
||||
*
|
||||
* @return InvalidEmail
|
||||
* @return InvalidEmail|null
|
||||
*/
|
||||
public function getError() : ?InvalidEmail;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 can‘t 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']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user