Message-id validator (#290)

* scafolding for MessageID validation

* Improve domain valid tokens

* Improved EmailParser to remove leaked logic. User of lexer recorder within parsers.

* MessageIDParser passing tests.

* change left for right, which is the right one

* psaml errors

* Better naming

* comments are not allowed in IDLeft for message-id

* Suppress psalm inheritance over tokens and dependencies

* Update src/Parser.php

Co-authored-by: Alexander M. Turek <me@derrabus.de>

* Update src/Parser.php

Co-authored-by: Alexander M. Turek <me@derrabus.de>

* improve parser from comments

Co-authored-by: Alexander M. Turek <me@derrabus.de>
This commit is contained in:
Eduardo Gulias Davis
2021-03-06 18:29:10 +01:00
committed by GitHub
parent 451b438902
commit 8e526a57c1
17 changed files with 559 additions and 242 deletions
Generated
+99 -98
View File
@@ -88,16 +88,16 @@
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44"
"reference": "2d63434d922daf7da8dd863e7907e67ee3031483"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44",
"reference": "0eb8293dbbcd6ef6bf81404c9ce7d95bcdf34f44",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/2d63434d922daf7da8dd863e7907e67ee3031483",
"reference": "2d63434d922daf7da8dd863e7907e67ee3031483",
"shasum": ""
},
"require": {
@@ -155,7 +155,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.22.1"
},
"funding": [
{
@@ -171,20 +171,20 @@
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "6e971c891537eb617a00bb07a43d182a6915faba"
"reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba",
"reference": "6e971c891537eb617a00bb07a43d182a6915faba",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248",
"reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248",
"shasum": ""
},
"require": {
@@ -239,7 +239,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1"
},
"funding": [
{
@@ -255,11 +255,11 @@
"type": "tidelift"
}
],
"time": "2021-01-07T17:09:11+00:00"
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
@@ -315,7 +315,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php72/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-php72/tree/v1.22.1"
},
"funding": [
{
@@ -865,16 +865,16 @@
},
{
"name": "felixfbecker/language-server-protocol",
"version": "v1.5.0",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/felixfbecker/php-language-server-protocol.git",
"reference": "85e83cacd2ed573238678c6875f8f0d7ec699541"
"reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/85e83cacd2ed573238678c6875f8f0d7ec699541",
"reference": "85e83cacd2ed573238678c6875f8f0d7ec699541",
"url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
"reference": "9d846d1f5cf101deee7a61c8ba7caa0a975cd730",
"shasum": ""
},
"require": {
@@ -915,9 +915,9 @@
],
"support": {
"issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
"source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.0"
"source": "https://github.com/felixfbecker/php-language-server-protocol/tree/1.5.1"
},
"time": "2020-10-23T13:55:30+00:00"
"time": "2021-02-22T14:02:09+00:00"
},
{
"name": "guzzlehttp/guzzle",
@@ -1431,16 +1431,16 @@
},
{
"name": "phar-io/version",
"version": "3.0.4",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
"reference": "e4782611070e50613683d2b9a57730e9a3ba5451"
"reference": "bae7c545bef187884426f042434e561ab1ddb182"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451",
"reference": "e4782611070e50613683d2b9a57730e9a3ba5451",
"url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182",
"reference": "bae7c545bef187884426f042434e561ab1ddb182",
"shasum": ""
},
"require": {
@@ -1476,9 +1476,9 @@
"description": "Library for handling version information and constraints",
"support": {
"issues": "https://github.com/phar-io/version/issues",
"source": "https://github.com/phar-io/version/tree/3.0.4"
"source": "https://github.com/phar-io/version/tree/3.1.0"
},
"time": "2020-12-13T23:18:30+00:00"
"time": "2021-02-23T14:00:09+00:00"
},
{
"name": "php-coveralls/php-coveralls",
@@ -3165,16 +3165,16 @@
},
{
"name": "symfony/config",
"version": "v5.2.1",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "d0a82d965296083fe463d655a3644cbe49cbaa80"
"reference": "50e0e1314a3b2609d32b6a5a0d0fb5342494c4ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/d0a82d965296083fe463d655a3644cbe49cbaa80",
"reference": "d0a82d965296083fe463d655a3644cbe49cbaa80",
"url": "https://api.github.com/repos/symfony/config/zipball/50e0e1314a3b2609d32b6a5a0d0fb5342494c4ab",
"reference": "50e0e1314a3b2609d32b6a5a0d0fb5342494c4ab",
"shasum": ""
},
"require": {
@@ -3220,10 +3220,10 @@
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Config Component",
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/config/tree/v5.2.1"
"source": "https://github.com/symfony/config/tree/v5.2.3"
},
"funding": [
{
@@ -3239,20 +3239,20 @@
"type": "tidelift"
}
],
"time": "2020-12-09T18:54:12+00:00"
"time": "2021-01-27T10:15:41+00:00"
},
{
"name": "symfony/console",
"version": "v5.2.1",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "47c02526c532fb381374dab26df05e7313978976"
"reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/47c02526c532fb381374dab26df05e7313978976",
"reference": "47c02526c532fb381374dab26df05e7313978976",
"url": "https://api.github.com/repos/symfony/console/zipball/89d4b176d12a2946a1ae4e34906a025b7b6b135a",
"reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a",
"shasum": ""
},
"require": {
@@ -3311,7 +3311,7 @@
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Console Component",
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"keywords": [
"cli",
@@ -3320,7 +3320,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.2.1"
"source": "https://github.com/symfony/console/tree/v5.2.3"
},
"funding": [
{
@@ -3336,7 +3336,7 @@
"type": "tidelift"
}
],
"time": "2020-12-18T08:03:05+00:00"
"time": "2021-01-28T22:06:19+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -3407,16 +3407,16 @@
},
{
"name": "symfony/filesystem",
"version": "v5.2.1",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d"
"reference": "262d033b57c73e8b59cd6e68a45c528318b15038"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/fa8f8cab6b65e2d99a118e082935344c5ba8c60d",
"reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/262d033b57c73e8b59cd6e68a45c528318b15038",
"reference": "262d033b57c73e8b59cd6e68a45c528318b15038",
"shasum": ""
},
"require": {
@@ -3446,10 +3446,10 @@
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Filesystem Component",
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v5.2.1"
"source": "https://github.com/symfony/filesystem/tree/v5.2.3"
},
"funding": [
{
@@ -3465,11 +3465,11 @@
"type": "tidelift"
}
],
"time": "2020-11-30T17:05:38+00:00"
"time": "2021-01-27T10:01:46+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -3528,7 +3528,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1"
},
"funding": [
{
@@ -3548,16 +3548,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "267a9adeb8ecb8071040a740930e077cdfb987af"
"reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/267a9adeb8ecb8071040a740930e077cdfb987af",
"reference": "267a9adeb8ecb8071040a740930e077cdfb987af",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170",
"reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170",
"shasum": ""
},
"require": {
@@ -3609,7 +3609,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1"
},
"funding": [
{
@@ -3625,20 +3625,20 @@
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13"
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13",
"reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
"shasum": ""
},
"require": {
@@ -3689,7 +3689,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1"
},
"funding": [
{
@@ -3705,11 +3705,11 @@
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-php73",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php73.git",
@@ -3768,7 +3768,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1"
},
"funding": [
{
@@ -3788,7 +3788,7 @@
},
{
"name": "symfony/polyfill-php80",
"version": "v1.22.0",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
@@ -3851,7 +3851,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.22.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1"
},
"funding": [
{
@@ -3950,16 +3950,16 @@
},
{
"name": "symfony/stopwatch",
"version": "v5.2.1",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
"reference": "2b105c0354f39a63038a1d8bf776ee92852813af"
"reference": "b12274acfab9d9850c52583d136a24398cdf1a0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/2b105c0354f39a63038a1d8bf776ee92852813af",
"reference": "2b105c0354f39a63038a1d8bf776ee92852813af",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/b12274acfab9d9850c52583d136a24398cdf1a0c",
"reference": "b12274acfab9d9850c52583d136a24398cdf1a0c",
"shasum": ""
},
"require": {
@@ -3989,10 +3989,10 @@
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Stopwatch Component",
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/stopwatch/tree/v5.2.1"
"source": "https://github.com/symfony/stopwatch/tree/v5.2.3"
},
"funding": [
{
@@ -4008,20 +4008,20 @@
"type": "tidelift"
}
],
"time": "2020-11-01T16:14:45+00:00"
"time": "2021-01-27T10:15:41+00:00"
},
{
"name": "symfony/string",
"version": "v5.2.1",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed"
"reference": "c95468897f408dd0aca2ff582074423dd0455122"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed",
"reference": "5bd67751d2e3f7d6f770c9154b8fbcb2aa05f7ed",
"url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122",
"reference": "c95468897f408dd0aca2ff582074423dd0455122",
"shasum": ""
},
"require": {
@@ -4064,7 +4064,7 @@
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony String component",
"description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
"homepage": "https://symfony.com",
"keywords": [
"grapheme",
@@ -4075,7 +4075,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.2.1"
"source": "https://github.com/symfony/string/tree/v5.2.3"
},
"funding": [
{
@@ -4091,20 +4091,20 @@
"type": "tidelift"
}
],
"time": "2020-12-05T07:33:16+00:00"
"time": "2021-01-25T15:14:59+00:00"
},
{
"name": "symfony/yaml",
"version": "v5.2.1",
"version": "v5.2.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "290ea5e03b8cf9b42c783163123f54441fb06939"
"reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/290ea5e03b8cf9b42c783163123f54441fb06939",
"reference": "290ea5e03b8cf9b42c783163123f54441fb06939",
"url": "https://api.github.com/repos/symfony/yaml/zipball/338cddc6d74929f6adf19ca5682ac4b8e109cdb0",
"reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0",
"shasum": ""
},
"require": {
@@ -4147,10 +4147,10 @@
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v5.2.1"
"source": "https://github.com/symfony/yaml/tree/v5.2.3"
},
"funding": [
{
@@ -4166,7 +4166,7 @@
"type": "tidelift"
}
],
"time": "2020-12-08T17:02:38+00:00"
"time": "2021-02-03T04:42:09+00:00"
},
{
"name": "theseer/tokenizer",
@@ -4220,16 +4220,16 @@
},
{
"name": "vimeo/psalm",
"version": "4.4.1",
"version": "4.6.2",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
"reference": "9fd7a7d885b3a216cff8dec9d8c21a132f275224"
"reference": "bca09d74adc704c4eaee36a3c3e9d379e290fc3b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/9fd7a7d885b3a216cff8dec9d8c21a132f275224",
"reference": "9fd7a7d885b3a216cff8dec9d8c21a132f275224",
"url": "https://api.github.com/repos/vimeo/psalm/zipball/bca09d74adc704c4eaee36a3c3e9d379e290fc3b",
"reference": "bca09d74adc704c4eaee36a3c3e9d379e290fc3b",
"shasum": ""
},
"require": {
@@ -4246,8 +4246,8 @@
"ext-simplexml": "*",
"ext-tokenizer": "*",
"felixfbecker/advanced-json-rpc": "^3.0.3",
"felixfbecker/language-server-protocol": "^1.4",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0",
"felixfbecker/language-server-protocol": "^1.5",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"nikic/php-parser": "^4.10.1",
"openlss/lib-array2xml": "^1.0",
"php": "^7.1|^8",
@@ -4263,6 +4263,7 @@
"bamarni/composer-bin-plugin": "^1.2",
"brianium/paratest": "^4.0||^6.0",
"ext-curl": "*",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpdocumentor/reflection-docblock": "^5",
"phpmyadmin/sql-parser": "5.1.0||dev-master",
"phpspec/prophecy": ">=1.9.0",
@@ -4318,21 +4319,21 @@
],
"support": {
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm/tree/4.4.1"
"source": "https://github.com/vimeo/psalm/tree/4.6.2"
},
"time": "2021-01-14T21:44:29+00:00"
"time": "2021-02-26T02:24:18+00:00"
},
{
"name": "webmozart/assert",
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
"reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
"shasum": ""
},
@@ -4370,8 +4371,8 @@
"validate"
],
"support": {
"issues": "https://github.com/webmozart/assert/issues",
"source": "https://github.com/webmozart/assert/tree/master"
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.9.1"
},
"time": "2020-07-08T17:02:28+00:00"
},
+1
View File
@@ -124,6 +124,7 @@ class EmailLexer extends AbstractLexer
*
* @var array
*
* @psalm-suppress NonInvariantDocblockPropertyType
* @psalm-var array{value:string, type:null|int, position:int}
* @psalm-suppress NonInvariantDocblockPropertyType
*/
+23 -74
View File
@@ -9,18 +9,12 @@ use Egulias\EmailValidator\Parser\DomainPart;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\EmailTooLong;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Result\Reason\NoLocalPart;
class EmailParser
class EmailParser extends Parser
{
const EMAIL_MAX_LENGTH = 254;
/**
* @var array
*/
protected $warnings = [];
/**
* @var string
*/
@@ -30,52 +24,39 @@ class EmailParser
* @var string
*/
protected $localPart = '';
/**
* @var EmailLexer
*/
protected $lexer;
public function __construct(EmailLexer $lexer)
{
$this->lexer = $lexer;
}
public function parse(string $str) : Result
{
$this->lexer->setInput($str);
if (!$this->hasAtToken()) {
return new InvalidEmail(new NoLocalPart(), $this->lexer->token["value"]);
}
$localPartResult = $this->processLocalPart();
if ($localPartResult->isInvalid()) {
return $localPartResult;
}
$domainPartResult = $this->processDomainPart();
if ($domainPartResult->isInvalid()) {
return $domainPartResult;
}
if ($this->lexer->hasInvalidTokens()) {
return new InvalidEmail(new ExpectingATEXT("Invalid tokens found"), $this->lexer->token["value"]);
}
$result = parent::parse($str);
$this->addLongEmailWarning($this->localPart, $this->domainPart);
return $result;
}
protected function preLeftParsing(): Result
{
if (!$this->hasAtToken()) {
return new InvalidEmail(new NoLocalPart(), $this->lexer->token["value"]);
}
return new ValidEmail();
}
protected function parseLeftFromAt(): Result
{
return $this->processLocalPart();
}
protected function parseRightFromAt(): Result
{
return $this->processDomainPart();
}
private function processLocalPart() : Result
{
$this->lexer->startRecording();
$localPartParser = new LocalPart($this->lexer);
$localPartResult = $localPartParser->parse();
$this->lexer->stopRecording();
$this->localPart = rtrim($this->lexer->getAccumulatedValues(), '@');
$this->localPart = $localPartParser->localPart();
$this->warnings = array_merge($localPartParser->getWarnings(), $this->warnings);
return $localPartResult;
@@ -83,28 +64,14 @@ class EmailParser
private function processDomainPart() : Result
{
$this->lexer->clearRecorded();
$this->lexer->startRecording();
$domainPartParser = new DomainPart($this->lexer);
$domainPartResult = $domainPartParser->parse();
$this->lexer->stopRecording();
$this->domainPart = $this->lexer->getAccumulatedValues();
$this->domainPart = $domainPartParser->domainPart();
$this->warnings = array_merge($domainPartParser->getWarnings(), $this->warnings);
return $domainPartResult;
}
/**
* @return Warning\Warning[]
*/
public function getWarnings() : array
{
return $this->warnings;
}
/**
* @return string
*/
public function getDomainPart() : string
{
return $this->domainPart;
@@ -115,25 +82,7 @@ class EmailParser
return $this->localPart;
}
/**
* @return bool
*/
protected function hasAtToken() : bool
{
$this->lexer->moveNext();
$this->lexer->moveNext();
if ($this->lexer->token['type'] === EmailLexer::S_AT) {
return false;
}
return true;
}
/**
* @param string $localPart
* @param string $parsedDomainPart
*/
protected function addLongEmailWarning($localPart, $parsedDomainPart) : void
private function addLongEmailWarning(string $localPart, string $parsedDomainPart) : void
{
if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAIL_MAX_LENGTH) {
$this->warnings[EmailTooLong::CODE] = new EmailTooLong();
+93
View File
@@ -0,0 +1,93 @@
<?php
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;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Warning\EmailTooLong;
use Egulias\EmailValidator\Result\Reason\NoLocalPart;
class MessageIDParser extends Parser
{
const EMAILID_MAX_LENGTH = 254;
/**
* @var string
*/
protected $idLeft = '';
/**
* @var string
*/
protected $idRight = '';
public function parse(string $str) : Result
{
$result = parent::parse($str);
$this->addLongEmailWarning($this->idLeft, $this->idRight);
return $result;
}
protected function preLeftParsing(): Result
{
if (!$this->hasAtToken()) {
return new InvalidEmail(new NoLocalPart(), $this->lexer->token["value"]);
}
return new ValidEmail();
}
protected function parseLeftFromAt(): Result
{
return $this->processIDLeft();
}
protected function parseRightFromAt(): Result
{
return $this->processIDRight();
}
private function processIDLeft() : Result
{
$localPartParser = new IDLeftPart($this->lexer);
$localPartResult = $localPartParser->parse();
$this->idLeft = $localPartParser->localPart();
$this->warnings = array_merge($localPartParser->getWarnings(), $this->warnings);
return $localPartResult;
}
private function processIDRight() : Result
{
$domainPartParser = new IDRightPart($this->lexer);
$domainPartResult = $domainPartParser->parse();
$this->idRight = $domainPartParser->domainPart();
$this->warnings = array_merge($domainPartParser->getWarnings(), $this->warnings);
return $domainPartResult;
}
public function getLeftPart() : string
{
return $this->idLeft;
}
public function getRightPart() : string
{
return $this->idRight;
}
private function addLongEmailWarning(string $localPart, string $parsedDomainPart) : void
{
if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAILID_MAX_LENGTH) {
$this->warnings[EmailTooLong::CODE] = new EmailTooLong();
}
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Egulias\EmailValidator;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
abstract class Parser
{
/**
* @var Warning\Warning[]
*/
protected $warnings = [];
/**
* @var EmailLexer
*/
protected $lexer;
/**
* id-left "@" id-right
*/
abstract protected function parseRightFromAt() : Result;
abstract protected function parseLeftFromAt() : Result;
abstract protected function preLeftParsing() : Result;
public function __construct(EmailLexer $lexer)
{
$this->lexer = $lexer;
}
public function parse(string $str) : Result
{
$this->lexer->setInput($str);
if ($this->lexer->hasInvalidTokens()) {
return new InvalidEmail(new ExpectingATEXT("Invalid tokens found"), $this->lexer->token["value"]);
}
$preParsingResult = $this->preLeftParsing();
if ($preParsingResult->isInvalid()) {
return $preParsingResult;
}
$localPartResult = $this->parseLeftFromAt();
if ($localPartResult->isInvalid()) {
return $localPartResult;
}
$domainPartResult = $this->parseRightFromAt();
if ($domainPartResult->isInvalid()) {
return $domainPartResult;
}
return new ValidEmail();
}
/**
* @return Warning\Warning[]
*/
public function getWarnings() : array
{
return $this->warnings;
}
protected function hasAtToken() : bool
{
$this->lexer->moveNext();
$this->lexer->moveNext();
return $this->lexer->token['type'] !== EmailLexer::S_AT;
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ use Egulias\EmailValidator\Result\Reason\UnclosedComment;
use Egulias\EmailValidator\Result\Reason\UnOpenedComment;
use Egulias\EmailValidator\Warning\Comment as WarningComment;
class Comment extends Parser
class Comment extends PartParser
{
/**
* @var int
+1 -1
View File
@@ -20,7 +20,7 @@ use Egulias\EmailValidator\Result\Reason\ExpectingDTEXT;
use Egulias\EmailValidator\Result\Reason\UnusualElements;
use Egulias\EmailValidator\Warning\DomainLiteral as WarningDomainLiteral;
class DomainLiteral extends Parser
class DomainLiteral extends PartParser
{
public function parse() : Result
{
+33 -31
View File
@@ -22,7 +22,7 @@ use Egulias\EmailValidator\Parser\CommentStrategy\DomainComment;
use Egulias\EmailValidator\Result\Reason\ExpectingDomainLiteralClose;
use Egulias\EmailValidator\Parser\DomainLiteral as DomainLiteralParser;
class DomainPart extends Parser
class DomainPart extends PartParser
{
const DOMAIN_MAX_LENGTH = 253;
const LABEL_MAX_LENGTH = 63;
@@ -39,6 +39,9 @@ class DomainPart extends Parser
public function parse() : Result
{
$this->lexer->clearRecorded();
$this->lexer->startRecording();
$this->lexer->moveNext();
$domainChecks = $this->performDomainStartChecks();
@@ -49,20 +52,22 @@ class DomainPart extends Parser
if ($this->lexer->token['type'] === EmailLexer::S_AT) {
return new InvalidEmail(new ConsecutiveAt(), $this->lexer->token['value']);
}
$domain = $this->doParseDomainPart();
if ($domain->isInvalid()) {
return $domain;
}
$length = strlen($this->domainPart);
$result = $this->doParseDomainPart();
if ($result->isInvalid()) {
return $result;
}
$end = $this->checkEndOfDomain();
if ($end->isInvalid()) {
return $end;
}
$this->lexer->stopRecording();
$this->domainPart = $this->lexer->getAccumulatedValues();
$length = strlen($this->domainPart);
if ($length > self::DOMAIN_MAX_LENGTH) {
//$this->warnings[DomainTooLong::CODE] = new DomainTooLong();
return new InvalidEmail(new DomainTooLong(), $this->lexer->token['value']);
}
@@ -128,14 +133,6 @@ class DomainPart extends Parser
return new ValidEmail();
}
/**
* @return string
*/
public function getDomainPart()
{
return $this->domainPart;
}
protected function parseComments(): Result
{
$commentParser = new Comment($this->lexer, new DomainComment());
@@ -229,7 +226,6 @@ class DomainPart extends Parser
*/
protected function parseDomainLiteral() : Result
{
try {
$this->lexer->find(EmailLexer::S_CLOSEBRACKET);
} catch (\RuntimeException $e) {
@@ -242,22 +238,8 @@ class DomainPart extends Parser
return $result;
}
/**
* @return InvalidEmail|ValidEmail
*/
protected function checkDomainPartExceptions(array $prev, bool $hasComments) : Result
{
$validDomainTokens = array(
EmailLexer::GENERIC => true,
EmailLexer::S_HYPHEN => true,
EmailLexer::S_DOT => true,
);
if ($hasComments) {
$validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
$validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
}
if ($this->lexer->token['type'] === EmailLexer::S_OPENBRACKET && $prev['type'] !== EmailLexer::S_AT) {
return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->token['value']);
}
@@ -271,6 +253,22 @@ class DomainPart extends Parser
return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->token['value']);
}
return $this->validateTokens($hasComments);
}
protected function validateTokens(bool $hasComments) : Result
{
$validDomainTokens = array(
EmailLexer::GENERIC => true,
EmailLexer::S_HYPHEN => true,
EmailLexer::S_DOT => true,
);
if ($hasComments) {
$validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true;
$validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true;
}
if (!isset($validDomainTokens[$this->lexer->token['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->token['value']), $this->lexer->token['value']);
}
@@ -278,7 +276,6 @@ class DomainPart extends Parser
return new ValidEmail();
}
private function checkLabelLength(bool $isEndOfDomain = false) : Result
{
if ($this->lexer->token['type'] === EmailLexer::S_DOT || $isEndOfDomain) {
@@ -307,4 +304,9 @@ class DomainPart extends Parser
$this->warnings[TLD::CODE] = new TLD();
}
}
public function domainPart() : string
{
return $this->domainPart;
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Result\Reason\UnclosedQuotedString;
use Egulias\EmailValidator\Result\Result;
class DoubleQuote extends Parser
class DoubleQuote extends PartParser
{
public function parse() : Result
{
+9 -10
View File
@@ -13,7 +13,7 @@ use Egulias\EmailValidator\Result\Reason\ExpectingCTEXT;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
class FoldingWhiteSpace extends Parser
class FoldingWhiteSpace extends PartParser
{
public function parse() : Result
{
@@ -23,7 +23,10 @@ class FoldingWhiteSpace extends Parser
$previous = $this->lexer->getPrevious();
$this->checkCRLFInFWS();
$resultCRLF = $this->checkCRLFInFWS();
if ($resultCRLF->isInvalid()) {
return $resultCRLF;
}
if ($this->lexer->token['type'] === EmailLexer::S_CR) {
return new InvalidEmail(new CRNoLF(), $this->lexer->token['value']);
@@ -46,10 +49,7 @@ class FoldingWhiteSpace extends Parser
return new ValidEmail();
}
/**
* @return InvalidEmail|ValidEmail|null
*/
protected function checkCRLFInFWS()
protected function checkCRLFInFWS() : Result
{
if ($this->lexer->token['type'] !== EmailLexer::CRLF) {
return new ValidEmail();
@@ -63,12 +63,11 @@ class FoldingWhiteSpace extends Parser
if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) {
return new InvalidEmail(new CRLFAtTheEnd(), $this->lexer->token['value']);
}
return new ValidEmail();
}
/**
* @return bool
*/
protected function isFWS()
protected function isFWS() : bool
{
if ($this->escaped()) {
return false;
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Parser\LocalPart;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\CommentsInIDRight;
class IDLeftPart extends LocalPart
{
protected function parseComments(): Result
{
return new InvalidEmail(new CommentsInIDRight(), $this->lexer->token['value']);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Egulias\EmailValidator\Parser;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
class IDRightPart extends DomainPart
{
protected function validateTokens(bool $hasComments) : Result
{
$invalidDomainTokens = array(
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']);
}
return new ValidEmail();
}
}
+35 -20
View File
@@ -13,27 +13,17 @@ use Egulias\EmailValidator\Result\Reason\ConsecutiveDot;
use Egulias\EmailValidator\Result\Reason\ExpectingATEXT;
use Egulias\EmailValidator\Parser\CommentStrategy\LocalComment;
class LocalPart extends Parser
class LocalPart extends PartParser
{
/**
* Invalid lexer tokens for local part
* @var array
* @var string
*/
private $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
);
private $localPart = '';
public function parse() : Result
{
$totalLength = 0;
$this->lexer->startRecording();
while ($this->lexer->token['type'] !== EmailLexer::S_AT && null !== $this->lexer->token['type']) {
if ($this->hasDotAtStart()) {
@@ -74,8 +64,9 @@ class LocalPart extends Parser
return $resultEscaping;
}
if (isset($this->invalidTokens[$this->lexer->token['type']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->token['value']);
$resultToken = $this->validateTokens(false);
if ($resultToken->isInvalid()) {
return $resultToken;
}
$resultFWS = $this->parseLocalFWS();
@@ -83,17 +74,41 @@ class LocalPart extends Parser
return $resultFWS;
}
$totalLength += strlen($this->lexer->token['value']);
$this->lexer->moveNext();
}
if ($totalLength > LocalTooLong::LOCAL_PART_LENGTH) {
$this->lexer->stopRecording();
$this->localPart = rtrim($this->lexer->getAccumulatedValues(), '@');
if (strlen($this->localPart) > LocalTooLong::LOCAL_PART_LENGTH) {
$this->warnings[LocalTooLong::CODE] = new LocalTooLong();
}
return new ValidEmail();
}
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']])) {
return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->token['value']);
}
return new ValidEmail();
}
public function localPart() : string
{
return $this->localPart;
}
private function parseLocalFWS() : Result
{
$foldingWS = new FoldingWhiteSpace($this->lexer);
@@ -118,7 +133,7 @@ class LocalPart extends Parser
return $parseAgain;
}
private function parseComments(): Result
protected function parseComments(): Result
{
$commentParser = new Comment($this->lexer, new LocalComment());
$result = $commentParser->parse();
@@ -8,7 +8,7 @@ use Egulias\EmailValidator\Result\Reason\ConsecutiveDot;
use Egulias\EmailValidator\Result\Result;
use Egulias\EmailValidator\Result\ValidEmail;
abstract class Parser
abstract class PartParser
{
/**
* @var array
@@ -25,6 +25,8 @@ abstract class Parser
$this->lexer = $lexer;
}
abstract public function parse() : Result;
/**
* @return \Egulias\EmailValidator\Warning\Warning[]
*/
@@ -33,8 +35,6 @@ abstract class Parser
return $this->warnings;
}
abstract public function parse() : Result;
protected function parseFWS() : Result
{
$foldingWS = new FoldingWhiteSpace($this->lexer);
@@ -52,9 +52,6 @@ abstract class Parser
return new ValidEmail();
}
/**
* @return bool
*/
protected function escaped() : bool
{
$previous = $this->lexer->getPrevious();
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace Egulias\EmailValidator\Result\Reason;
class CommentsInIDRight implements Reason
{
public function code() : int
{
return 400;
}
public function description() : string
{
return 'Comments are not allowed in IDRight for message-id';
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Egulias\EmailValidator\Validation;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\MessageIDParser;
use Egulias\EmailValidator\Result\InvalidEmail;
use Egulias\EmailValidator\Result\Reason\ExceptionFound;
class MessageIDValidation implements EmailValidation
{
/**
* @var array
*/
private $warnings = [];
/**
* @var ?InvalidEmail
*/
private $error;
public function isValid(string $email, EmailLexer $emailLexer): bool
{
$parser = new MessageIDParser($emailLexer);
try {
$result = $parser->parse($email);
$this->warnings = $parser->getWarnings();
if ($result->isInvalid()) {
/** @psalm-suppress PropertyTypeCoercion */
$this->error = $result;
return false;
}
} catch (\Exception $invalid) {
$this->error = new InvalidEmail(new ExceptionFound($invalid), '');
return false;
}
return true;
}
public function getWarnings(): array
{
return $this->warnings;
}
public function getError(): ?InvalidEmail
{
return $this->error;
}
}
@@ -0,0 +1,70 @@
<?php
namespace Egulias\EmailValidator\Tests\EmailValidator\Validation;
use Egulias\EmailValidator\EmailLexer;
use Egulias\EmailValidator\Validation\MessageIDValidation;
use PHPUnit\Framework\TestCase;
class MessageIDValidationTest extends TestCase
{
/**
* @dataProvider validMessageIDs
*/
public function testValidMessageIDs(string $messageID)
{
$validator = new MessageIDValidation();
$this->assertTrue($validator->isValid($messageID, new EmailLexer()));
}
public function validMessageIDs() : array
{
return [
['a@b.c+&%$.d'],
['a.b+&%$.c@d'],
['a@ä'],
];
}
/**
* @dataProvider invalidMessageIDs
*/
public function testInvalidMessageIDs(string $messageID)
{
$validator = new MessageIDValidation();
$this->assertFalse($validator->isValid($messageID, new EmailLexer()));
}
public function invalidMessageIDs() : array
{
return [
['example'],
['example@with space'],
['example@iana.'],
['example@ia\na.'],
/**
* RFC 2822, section 3.6.4, Page 25
* Since the msg-id has
* a similar syntax to angle-addr (identical except that comments and
* folding white space are not allowed), a good method is to put the
* domain name (or a domain literal IP address) of the host on which the
* message identifier was created on the right hand side of the "@", and
* put a combination of the current absolute date and time along with
* some other currently unique (perhaps sequential) identifier available
* on the system (for example, a process id number) on the left hand
* side.
*/
['example(comment)@example.com'],
["\r\nFWS@example.com"]
];
}
public function testInvalidMessageIDsWithError()
{
$this->markTestIncomplete("missing error check");
}
}