├── .editorconfig
├── .github
└── workflows
│ └── php.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── HELP.md
├── README.md
├── SECURITY.md
├── build
├── coverage
│ └── .keep
├── insights
│ └── .keep
├── phpcs
│ └── .keep
└── phpmd
│ └── .keep
├── composer.json
├── examples
├── disposableEmailList.php
├── disposableEmailListWithCustomDomains.php
├── fullValidation.php
└── getDisposableEmailList.php
├── license.txt
├── phpunit.xml
├── src
└── EmailValidator
│ ├── EmailAddress.php
│ ├── EmailValidator.php
│ ├── Policy.php
│ └── Validator
│ ├── AProviderValidator.php
│ ├── AValidator.php
│ ├── BannedListValidator.php
│ ├── BasicValidator.php
│ ├── DisposableEmailValidator.php
│ ├── Domain
│ ├── DomainLiteralValidator.php
│ └── DomainNameValidator.php
│ ├── FreeEmailValidator.php
│ ├── GmailValidator.php
│ ├── IValidator.php
│ ├── LocalPart
│ ├── AtomValidator.php
│ └── QuotedStringValidator.php
│ ├── MxValidator.php
│ └── Rfc5322Validator.php
└── tests
└── EmailValidator
├── EmailAddressTest.php
├── EmailValidatorTest.php
├── PolicyTest.php
└── Validator
├── BannedListValidatorTest.php
├── BasicValidatorTest.php
├── CustomValidatorTest.php
├── DisposableEmailValidatorTest.php
├── Domain
├── DomainLiteralValidatorTest.php
└── DomainNameValidatorTest.php
├── ExampleDotComValidator.php
├── FreeEmailValidatorTest.php
├── GmailValidatorTest.php
├── LocalPart
├── AtomValidatorTest.php
└── QuotedStringValidatorTest.php
├── MxValidatorTest.php
├── ProviderValidatorTest.php
└── Rfc5322ValidatorTest.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{php,phpt}]
12 | indent_style = space
13 | indent_size = 4
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 | max_line_length = 120
19 |
20 | [*.{yml,yaml,xml,json}]
21 | indent_style = space
22 | indent_size = 2
23 |
24 | [*.md]
25 | trim_trailing_whitespace = false
26 | max_line_length = off
27 |
28 | [*.{js,jsx,ts,tsx,css,scss}]
29 | indent_style = space
30 | indent_size = 2
31 |
--------------------------------------------------------------------------------
/.github/workflows/php.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | workflow_dispatch:
9 | branches: [ master ]
10 |
11 | jobs:
12 | build:
13 |
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - uses: shivammathur/setup-php@v2
20 | with:
21 | php-version: '7.4'
22 |
23 | - name: Validate composer.json and composer.lock
24 | run: composer validate
25 |
26 | - name: Suppress composer error message
27 | run: composer config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
28 |
29 | - name: Install dependencies
30 | run: composer install --prefer-dist --no-progress
31 |
32 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit"
33 | # Docs: https://getcomposer.org/doc/articles/scripts.md
34 |
35 | - name: Run test suite
36 | run: composer run-script test
37 |
38 | # - name: Run PHP Code Sniffer
39 | # run: composer run-script phpcs
40 | #
41 | # - name: Run PHP Mess Detecter
42 | # run: composer run-script phpmd
43 | #
44 | # - name: Run PHP Insights
45 | # run: composer run-script insights
46 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## 2.1.0 - 2025-06-10
9 |
10 | ### Added
11 | - Comprehensive RFC 5322 email validation:
12 | - Added new `Rfc5322Validator` class for strict RFC compliance
13 | - Added support for quoted strings in local part
14 | - Added proper domain literal (IP address) validation
15 | - Added comment extraction and validation
16 | - Added length validation for local part and domain components
17 | - Added new error code `FAIL_RFC5322` for RFC validation failures
18 | - Enhanced `EmailAddress` class:
19 | - Added `getLocalPart()` method
20 | - Added `getComments()` method
21 | - Improved comment parsing with nested comment support
22 | - Better handling of quoted strings and escaped characters
23 |
24 | ## 2.0.0 - 2025-05-29
25 |
26 | ### Added
27 | - Strict type declarations for all properties and method signatures.
28 | - Expanded unit tests to cover edge cases, type safety, and invalid input handling.
29 | - Added support for custom validators through the new `registerValidator()` method.
30 | - Added new error code `FAIL_CUSTOM` for custom validation failures.
31 |
32 | ### Changed
33 | - **Updated the minimum PHP version requirement to PHP 7.4**.
34 | - Refactored internal logic for better null safety and array filtering.
35 | - Refactored provider validators to handle null/invalid domains gracefully and consistently return true for invalid emails.
36 | - Improved type safety with PHP 7.4+ typed properties and enhanced PHPDoc array type hints.
37 | - Updated `.editorconfig` to be more explicit for certain file types, including PSR-12 for PHP.
38 |
39 | ### Fixed
40 | - Issue #7: Improved email address parsing to properly handle RFC822 compliant addresses:
41 | - Multiple @ symbols in quoted strings
42 | - Domain literals (IP addresses in square brackets)
43 | - Comments in email addresses
44 | - Better handling of edge cases
45 |
46 | ## [1.1.4] - 2024-04-09
47 |
48 | ### Changed
49 | - CHANGELOG format
50 |
51 | ### Fixed
52 | - Issue #5: Static variables prevent running validation with different configurations
53 | - Issue #6: `googlemail.com` is now recognized as a Gmail address
54 | - Issue #6: `.` are now removed when sanitizing Gmail addresses (to get to the root email address)
55 |
56 | ## [1.1.3] - 2022-10-12
57 |
58 | ### Fixed
59 |
60 | - Handled potential for null being returned when validating a banned domain name
61 |
62 | ## [1.1.1] - 2022-10-11
63 |
64 | ### Changed
65 |
66 | - Banned domain check to use pattern matching for more robust validation including subdomains
67 |
68 | ## [1.1.1] - 2022-02-22
69 |
70 | ### Fixed
71 |
72 | - When getting an email address' username, if there was none, return an empty string instead of NULL
73 |
74 | ## [1.1.0] - 2022-02-02
75 |
76 | ### Added
77 |
78 | - Support for identifying and working with Gmail addresses using the "plus trick" to create unique addresses
79 |
80 | ## [1.0.2] - 2022-01-24
81 |
82 | ### Fixed
83 |
84 | - Issue #2: Error state not clearing between validations
85 |
86 | ## [1.0.1] - 2021-09-20
87 |
88 | ### Added
89 |
90 | - Pull Request #1: Added EmailValidator::getErrorCode()
91 |
92 | ## [1.0.0] - 2020-08-02
93 |
94 | - Initial release
95 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Code of Conduct
9 |
10 | ### Our Pledge
11 |
12 | In the interest of fostering an open and welcoming environment, we as
13 | contributors and maintainers pledge to making participation in our project and
14 | our community a harassment-free experience for everyone, regardless of age, body
15 | size, disability, ethnicity, gender identity and expression, level of experience,
16 | nationality, personal appearance, race, religion, or sexual identity and
17 | orientation.
18 |
19 | ### Our Standards
20 |
21 | Examples of behavior that contributes to creating a positive environment
22 | include:
23 |
24 | * Using welcoming and inclusive language
25 | * Being respectful of differing viewpoints and experiences
26 | * Gracefully accepting constructive criticism
27 | * Focusing on what is best for the community
28 | * Showing empathy towards other community members
29 |
30 | Examples of unacceptable behavior by participants include:
31 |
32 | * The use of sexualized language or imagery and unwelcome sexual attention or
33 | advances
34 | * Trolling, insulting/derogatory comments, and personal or political attacks
35 | * Public or private harassment
36 | * Publishing others' private information, such as a physical or electronic
37 | address, without explicit permission
38 | * Other conduct which could reasonably be considered inappropriate in a
39 | professional setting
40 |
41 | ### Our Responsibilities
42 |
43 | Project maintainers are responsible for clarifying the standards of acceptable
44 | behavior and are expected to take appropriate and fair corrective action in
45 | response to any instances of unacceptable behavior.
46 |
47 | Project maintainers have the right and responsibility to remove, edit, or
48 | reject comments, commits, code, wiki edits, issues, and other contributions
49 | that are not aligned to this Code of Conduct, or to ban temporarily or
50 | permanently any contributor for other behaviors that they deem inappropriate,
51 | threatening, offensive, or harmful.
52 |
53 | ### Scope
54 |
55 | This Code of Conduct applies both within project spaces and in public spaces
56 | when an individual is representing the project or its community. Examples of
57 | representing a project or community include using an official project e-mail
58 | address, posting via an official social media account, or acting as an appointed
59 | representative at an online or offline event. Representation of a project may be
60 | further defined and clarified by project maintainers.
61 |
62 | ### Enforcement
63 |
64 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
65 | reported by contacting the project team at stymiee@gmail.com. All
66 | complaints will be reviewed and investigated and will result in a response that
67 | is deemed necessary and appropriate to the circumstances. The project team is
68 | obligated to maintain confidentiality with regard to the reporter of an incident.
69 | Further details of specific enforcement policies may be posted separately.
70 |
71 | Project maintainers who do not follow or enforce the Code of Conduct in good
72 | faith may face temporary or permanent repercussions as determined by other
73 | members of the project's leadership.
74 |
75 | ### Attribution
76 |
77 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
78 | available at [http://contributor-covenant.org/version/1/4][version]
79 |
80 | [homepage]: http://contributor-covenant.org
81 | [version]: http://contributor-covenant.org/version/1/4/
82 |
--------------------------------------------------------------------------------
/HELP.md:
--------------------------------------------------------------------------------
1 | # Help
2 | Here are some tips, solutions to common problems, and guides for testing.
3 |
4 | ## Tips
5 |
6 | ### Use default settings
7 |
8 | The default settings offer good accuracy that an email address is valid and is performant. If you wish to be stricter,
9 | enable `checkDisposableEmail` to prevent potentially low value users from utilizing your web application.
10 |
11 | ## Asking for help on Stack Overflow
12 | Be sure when you [ask a question](http://stackoverflow.com/questions/ask?tags=php,email,validation) pertaining to the
13 | usage of this library be sure to tag your question with the **PHP**, **email**, **validation** tags. Make sure you follow their
14 | [guide for asking a good question](http://stackoverflow.com/help/how-to-ask) as poorly asked questions will be closed,
15 | and I will not be able to assist you.
16 |
17 | A good question will include all the following:
18 | - A description of the problem (what are you trying to do? what results are you expecting? what results are you actually getting?)
19 | - The code you are using (only post the relevant code)
20 | - The output of your method call(s)
21 | - Any error message(s) you are getting
22 |
23 | **Do not use Stack Overflow to report bugs.** Bugs may be reported [here](https://github.com/stymiee/email-validator/issues/new).
24 |
25 | ## Useful Links
26 |
27 | - [Going Deeper Into Email Address Validation](https://www.johnconde.net/blog/going-deeper-into-email-address-validation/) (article)
28 | - [Tutorial: PHP Email Address Validator](https://www.johnconde.net/blog/tutorial-php-ema…ddress-validator/)
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://packagist.org/packages/stymiee/email-validator)
2 | [](https://packagist.org/packages/stymiee/email-validator)
3 | 
4 | [](https://scrutinizer-ci.com/g/stymiee/email-validator/?branch=master)
5 | [](https://packagist.org/packages/stymiee/email-validator)
6 |
7 | # PHP Email Validator (email-validator)
8 |
9 | The PHP Email Validator will validate an email address for all or some of the following conditions:
10 |
11 | - is in a valid format (supports both RFC 5321 and RFC 5322)
12 | - has configured MX records (optional)
13 | - is not a disposable email address (optional)
14 | - is not a free email account (optional)
15 | - is not a banned email domain (optional)
16 | - flag Gmail accounts that use the "plus trick" and return a sanitized email address
17 |
18 | The Email Validator is configurable, so you have full control over how much validation will occur.
19 |
20 | ## Requirements
21 |
22 | - PHP 7.4 or newer (v1.1.4 will work with PHP 7.2 or newer)
23 |
24 | ## Installation
25 |
26 | Simply add a dependency on `stymiee/email-validator` to your project's `composer.json` file if you use
27 | [Composer](https://getcomposer.org/) to manage the dependencies of your project.
28 |
29 | Here is a minimal example of a `composer.json` file that just defines a dependency on PHP Simple Encryption:
30 | ```json
31 | {
32 | "require": {
33 | "stymiee/email-validator": "^2"
34 | }
35 | }
36 | ```
37 | ## Functional Description
38 |
39 | The Email Validator library builds upon PHP's built in `filter_var($emailAddress, FILTER_VALIDATE_EMAIL);` by adding a
40 | default MX record check. It also offers additional validation against disposable email addresses, free email address
41 | providers, and a custom banned domain list.
42 |
43 | ### Validate MX
44 |
45 | If `checkMxRecords` is set to `true` in the configuration (see below) the domain name will be validated to ensure it
46 | exists and has MX records configured. If the domain does not exist or no MX records exist the odds are the email address
47 | is not in use.
48 |
49 | ### Restrict Disposable Email Addresses
50 |
51 | Many users who are abusing a system, or not using that system as intended, can use a disposable email service who
52 | provides a short-lived (approximately 10 minutes) email address to be used for registrations or user confirmations. If
53 | `checkDisposableEmail` is set to `true` in the configuration (see below) the domain name will be validated to ensure
54 | it is not associated with a disposable email address provider.
55 |
56 | You can add you own domains to this list if you find the public list providers do not have one you
57 | have identified in their lists. Examples are provided in the `examples` directory which demonstrate how to do this.
58 |
59 | ### Restrict Free Email Address Providers
60 |
61 | Many users who are abusing a system, or not using that system as intended, can use a free email service who
62 | provides a free email address which is immediately available to be used for registrations or user confirmations. If
63 | `checkFreeEmail` is set to `true` in the configuration (see below) the domain name will be validated to ensure
64 | it is not associated with a free email address provider.
65 |
66 | You can add you own domains to this list if you find the public list providers do not have one you
67 | have identified in their lists. Examples are provided in the `examples` directory which demonstrate how to do this.
68 |
69 | ### Restrict Banned Domains
70 |
71 | If you have users from a domain abusing your system, or you have business rules that require the blocking of certain
72 | domains (i.e. public email providers like Gmail or Yahoo mail), you can block then by setting `checkBannedListedEmail`
73 | to `true` in the configuration (see below) and providing an array of banned domains. Examples are provided in the
74 | `examples` directory which demonstrate how to do this.
75 |
76 | ### Flag Gmail Addresses Using The "Plus Trick"
77 |
78 | Gmail offers the ability to create unique email addresses within a Google account by adding a `+` character and unique
79 | identifier after the username portion of the email address. If not explicitly checked for a user can create an unlimited
80 | amount of unique email addresses that all belong to the same account.
81 |
82 | A special check can be performed when a Gmail account is used and a sanitized email address (e.g. one without the "plus
83 | trick") can be obtained and then checked for uniqueness in your system.
84 |
85 | ### Configuration
86 |
87 | To configure the Email Validator you can pass an array with the follow parameters/values:
88 |
89 | #### checkMxRecords
90 |
91 | A boolean value that enables/disables MX record validation. Enabled by default.
92 |
93 | #### checkBannedListedEmail
94 |
95 | A boolean value that enables/disables banned domain validation. Disabled by default.
96 |
97 | #### checkDisposableEmail
98 |
99 | A boolean value that enables/disables disposable email address validation. Disabled by default.
100 |
101 | #### checkFreeEmail
102 |
103 | A boolean value that enables/disables free email address provider validation. Disabled by default.
104 |
105 | #### localDisposableOnly
106 |
107 | A boolean value that when set to `true` will not retrieve third party disposable email provider lists. Use this if you
108 | cache the list of providers locally which is useful when performance matters. Disabled by default.
109 |
110 | #### LocalFreeOnly
111 |
112 | A boolean value that when set to `true` will not retrieve third party free email provider lists. Use this if you
113 | cache the list of providers locally which is useful when performance matters. Disabled by default.
114 |
115 | #### bannedList
116 |
117 | An array of domains that are not allowed to be used for email addresses.
118 |
119 | #### disposableList
120 |
121 | An array of domains that are suspected disposable email address providers.
122 |
123 | #### freeList
124 |
125 | An array of domains that are free email address providers.
126 |
127 | **Example**
128 | ```php
129 | $config = [
130 | 'checkMxRecords' => true,
131 | 'checkBannedListedEmail' => true,
132 | 'checkDisposableEmail' => true,
133 | 'checkFreeEmail' => true,
134 | 'bannedList' => $bannedDomainList,
135 | 'disposableList' => $customDisposableEmailList,
136 | 'freeList' => $customFreeEmailList,
137 | ];
138 | $emailValidator = new EmailValidator($config);
139 | ````
140 | ### Example
141 | ```php
142 | true,
179 | 'checkBannedListedEmail' => true,
180 | 'checkDisposableEmail' => true,
181 | 'checkFreeEmail' => true,
182 | 'bannedList' => $bannedDomainList,
183 | 'disposableList' => $customDisposableEmailList,
184 | 'freeList' => $customFreeEmailList,
185 | ];
186 | $emailValidator = new EmailValidator($config);
187 |
188 | foreach ($testEmailAddresses as $emailAddress) {
189 | $emailIsValid = $emailValidator->validate($emailAddress);
190 | echo ($emailIsValid) ? 'Email is valid' : $emailValidator->getErrorReason();
191 | if ($emailValidator->isGmailWithPlusChar()) {
192 | printf(
193 | ' (Sanitized address: %s)',
194 | $emailValidator->getGmailAddressWithoutPlus()
195 | );
196 | }
197 | echo PHP_EOL;
198 | }
199 | ```
200 |
201 | **Output**
202 | ```
203 | Domain is banned
204 | Email is valid
205 | Domain is used by free email providers
206 | Domain is used by free email providers
207 | Domain is used by free email providers
208 | Domain is used by free email providers
209 | Domain is banned
210 | Domain does not accept email
211 | Domain is used by disposable email providers
212 | Domain is used by free email providers
213 | Domain is used by disposable email providers
214 | Domain does not accept email
215 | Domain is used by disposable email providers
216 | Domain is used by free email providers (Sanitized address: test@gmail.com)
217 | ```
218 | ## Custom Validators
219 |
220 | You can create your own custom validators by extending the `AValidator` class. Here's an example:
221 |
222 | ```php
223 | use EmailValidator\Validator\AValidator;
224 | use EmailValidator\EmailAddress;
225 | use EmailValidator\Policy;
226 |
227 | class MyCustomValidator extends AValidator
228 | {
229 | public function validate(EmailAddress $email): bool
230 | {
231 | // Your custom validation logic here
232 | return $email->getDomain() === 'example.com';
233 | }
234 | }
235 |
236 | // Register your custom validator
237 | $emailValidator = new EmailValidator();
238 | $emailValidator->registerValidator(new MyCustomValidator(new Policy()));
239 |
240 | // Use it like any other validator
241 | $isValid = $emailValidator->validate('user@example.com');
242 | ```
243 |
244 | Custom validators will be run after all built-in validators. If a custom validator fails, the error code will be set to `EmailValidator::FAIL_CUSTOM` and the error message will be "Failed custom validation".
245 |
246 | ### Best Practices
247 |
248 | 1. Keep your validation logic focused and single-purpose
249 | 2. Use the Policy class to make your validator configurable
250 | 3. Handle null domains and invalid emails gracefully
251 | 4. Add appropriate unit tests for your custom validator
252 | 5. Document your validator's requirements and behavior
253 |
254 | ### Example Use Cases
255 |
256 | - Domain-specific validation rules
257 | - Custom business logic for email validation
258 | - Integration with external services
259 | - Special character restrictions
260 | - Custom format requirements
261 |
262 | ### RFC 5322 Validation
263 |
264 | The library supports full RFC 5322 email validation, which includes:
265 | - Quoted strings in local parts
266 | - Comments in local parts and domains
267 | - Domain literals (IPv4 and IPv6)
268 | - International domain names (IDNA 2008)
269 |
270 | **Example using RFC 5322 validation**
271 | ```php
272 | true,
280 | 'useRfc5322' => true // Enable RFC 5322 validation
281 | ];
282 | $emailValidator = new EmailValidator($config);
283 |
284 | $testEmailAddresses = [
285 | // Standard email addresses
286 | 'user@example.com',
287 |
288 | // Quoted strings
289 | '"John Doe"@example.com',
290 | '"very.unusual.@.unusual.com"@example.com',
291 |
292 | // Comments
293 | 'user(comment)@example.com',
294 | 'user@(comment)example.com',
295 |
296 | // Domain literals
297 | 'user@[192.0.2.1]',
298 | 'user@[IPv6:2001:db8::1]',
299 |
300 | // International domains
301 | 'user@münchen.de',
302 | 'user@xn--mnchen-3ya.de' // Punycode
303 | ];
304 |
305 | foreach ($testEmailAddresses as $emailAddress) {
306 | $emailIsValid = $emailValidator->validate($emailAddress);
307 | echo ($emailIsValid) ? 'Email is valid' : $emailValidator->getErrorReason();
308 | echo PHP_EOL;
309 | }
310 | ```
311 |
312 | The RFC 5322 validator supports:
313 |
314 | 1. **Quoted Strings**
315 | - Allows special characters and whitespace in local part when quoted
316 | - Example: `"John Doe"@example.com`
317 | - Example: `"very.unusual.@.unusual.com"@example.com`
318 |
319 | 2. **Comments**
320 | - Supports comments in both local part and domain
321 | - Example: `user(comment)@example.com`
322 | - Example: `user@(comment)example.com`
323 |
324 | 3. **Domain Literals**
325 | - Supports both IPv4 and IPv6 addresses
326 | - Example: `user@[192.0.2.1]`
327 | - Example: `user@[IPv6:2001:db8::1]`
328 |
329 | 4. **International Domains**
330 | - Full IDNA 2008 support
331 | - Handles both Unicode and Punycode
332 | - Example: `user@münchen.de`
333 | - Example: `user@xn--mnchen-3ya.de`
334 |
335 | ## Notes
336 |
337 | The email address is checked against a list of known disposable email address providers which are aggregated from
338 | public disposable email address provider lists. This requires making HTTP requests to get the lists when validating
339 | the address.
340 |
341 | ## Support
342 |
343 | If you require assistance using this library start by viewing the [HELP.md](HELP.md) file included in this package. It
344 | includes common problems and solutions as well how to ask for additional assistance.
345 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | The following versions of Email Validator are currently being supported with security updates.
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 1.0.x | :white_check_mark: |
10 |
11 | ## Reporting a Vulnerability
12 |
13 | To report a security vulnerability please email me at stymiee@gmail.com.
14 |
--------------------------------------------------------------------------------
/build/coverage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stymiee/email-validator/8ee3be76b3d01081d3a91259c118e5dc2b4fb424/build/coverage/.keep
--------------------------------------------------------------------------------
/build/insights/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stymiee/email-validator/8ee3be76b3d01081d3a91259c118e5dc2b4fb424/build/insights/.keep
--------------------------------------------------------------------------------
/build/phpcs/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stymiee/email-validator/8ee3be76b3d01081d3a91259c118e5dc2b4fb424/build/phpcs/.keep
--------------------------------------------------------------------------------
/build/phpmd/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stymiee/email-validator/8ee3be76b3d01081d3a91259c118e5dc2b4fb424/build/phpmd/.keep
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stymiee/email-validator",
3 | "type": "library",
4 | "description": "A robust PHP 7.4+ email validation library that extends beyond basic validation with MX record checks, disposable email detection, and free email provider validation. Features include strict typing, custom validator support, internationalization (i18n), and an extensible architecture. Perfect for applications requiring thorough email verification with customizable validation rules.",
5 | "keywords": [
6 | "PHP",
7 | "email",
8 | "validation",
9 | "mx",
10 | "disposable",
11 | "free-email",
12 | "i18n",
13 | "internationalization",
14 | "custom-validators",
15 | "email-verification",
16 | "type-safe",
17 | "strict-typing"
18 | ],
19 | "homepage": "https://github.com/stymiee/email-validator",
20 | "license": "Apache-2.0",
21 | "authors": [
22 | {
23 | "name": "John Conde",
24 | "email": "stymiee@gmail.com",
25 | "homepage": "https://stymiee.dev",
26 | "role": "Developer"
27 | }
28 | ],
29 | "require": {
30 | "php": ">=7.4",
31 | "ext-json": "*",
32 | "ext-intl": "*"
33 | },
34 | "require-dev": {
35 | "phpunit/phpunit": "^9.6",
36 | "squizlabs/php_codesniffer": "^3.7",
37 | "phpmd/phpmd": "^2.15",
38 | "nunomaduro/phpinsights": "^2.8",
39 | "phpstan/phpstan": "^1.10",
40 | "vimeo/psalm": "^5.15"
41 | },
42 | "autoload": {
43 | "psr-4": {
44 | "EmailValidator\\": "src/EmailValidator/"
45 | }
46 | },
47 | "autoload-dev": {
48 | "psr-4": {
49 | "EmailValidator\\Tests\\": "tests/EmailValidator/"
50 | }
51 | },
52 | "scripts": {
53 | "test": "phpunit tests/EmailValidator",
54 | "test:coverage": "phpunit --coverage-html build/coverage",
55 | "phpcs": "phpcs ./src --report-file=build/phpcs/report.txt --runtime-set ignore_warnings_on_exit 1 --runtime-set ignore_errors_on_exit 1",
56 | "phpcs:fix": "phpcbf ./src",
57 | "phpmd": "phpmd src/ html cleancode --reportfile build/phpmd/report.html --ignore-violations-on-exit",
58 | "insights": "phpinsights analyse src/EmailValidator --format=console > build/insights/report.txt",
59 | "phpstan": "phpstan analyse src tests",
60 | "psalm": "psalm",
61 | "check": [
62 | "@test",
63 | "@phpcs",
64 | "@phpmd",
65 | "@insights",
66 | "@phpstan",
67 | "@psalm"
68 | ]
69 | },
70 | "config": {
71 | "sort-packages": true,
72 | "optimize-autoloader": true,
73 | "allow-plugins": {
74 | "dealerdirect/phpcodesniffer-composer-installer": true
75 | }
76 | },
77 | "minimum-stability": "stable",
78 | "prefer-stable": true
79 | }
80 |
--------------------------------------------------------------------------------
/examples/disposableEmailList.php:
--------------------------------------------------------------------------------
1 | true,
21 | ];
22 | $emailValidator = new EmailValidator($config);
23 |
24 | foreach ($testEmailAddresses as $emailAddress) {
25 | $emailIsValid = $emailValidator->validate($emailAddress);
26 | echo ($emailIsValid) ? 'Email is valid' : $emailValidator->getErrorReason();
27 | echo PHP_EOL;
28 | }
29 |
--------------------------------------------------------------------------------
/examples/disposableEmailListWithCustomDomains.php:
--------------------------------------------------------------------------------
1 | true,
25 | 'disposableList' => $customDisposableEmailList,
26 | ];
27 | $emailValidator = new EmailValidator($config);
28 |
29 | foreach ($testEmailAddresses as $emailAddress) {
30 | $emailIsValid = $emailValidator->validate($emailAddress);
31 | echo ($emailIsValid) ? 'Email is valid' : $emailValidator->getErrorReason();
32 | echo PHP_EOL;
33 | }
34 |
--------------------------------------------------------------------------------
/examples/fullValidation.php:
--------------------------------------------------------------------------------
1 | true,
38 | 'checkBannedListedEmail' => true,
39 | 'checkDisposableEmail' => true,
40 | 'checkFreeEmail' => true,
41 | 'bannedList' => $bannedDomainList,
42 | 'disposableList' => $customDisposableEmailList,
43 | 'freeList' => $customFreeEmailList,
44 | ];
45 | $emailValidator = new EmailValidator($config);
46 |
47 | foreach ($testEmailAddresses as $emailAddress) {
48 | $emailIsValid = $emailValidator->validate($emailAddress);
49 | echo ($emailIsValid) ? 'Email is valid' : $emailValidator->getErrorReason();
50 | if ($emailValidator->isGmailWithPlusChar()) {
51 | printf(
52 | ' (%s is a Gmail account and contains a plus character. Sanitized address: %s)',
53 | $emailAddress,
54 | $emailValidator->getGmailAddressWithoutPlus()
55 | );
56 | }
57 | echo PHP_EOL;
58 | }
59 |
--------------------------------------------------------------------------------
/examples/getDisposableEmailList.php:
--------------------------------------------------------------------------------
1 | getList();
9 |
10 | // Store this list somewhere for later use
11 | var_dump($disposableEmailList);
12 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 |
3 | Version 2.0, January 2004
4 |
5 | http://www.apache.org/licenses/
6 |
7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8 |
9 | 1. Definitions.
10 |
11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
16 |
17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
18 |
19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
20 |
21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
22 |
23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
24 |
25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
26 |
27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
28 |
29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
30 |
31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
32 |
33 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
34 |
35 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
36 |
37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and
38 | You must cause any modified files to carry prominent notices stating that You changed the files; and
39 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
40 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
41 |
42 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
44 |
45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
46 |
47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
48 |
49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
50 |
51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
52 |
53 | END OF TERMS AND CONDITIONS
54 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ./src
6 |
7 |
8 | vendor
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ./tests/EmailValidator/
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/EmailValidator/EmailAddress.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | private array $comments = [];
28 |
29 | public function __construct(string $email)
30 | {
31 | $this->email = $email;
32 | $this->parseEmail();
33 | }
34 |
35 | /**
36 | * Parses the email address into local part and domain
37 | *
38 | * This method handles:
39 | * - Multiple @ symbols in quoted strings
40 | * - Domain literals (IP addresses in square brackets)
41 | * - Comments in email addresses
42 | *
43 | * @return void
44 | */
45 | private function parseEmail(): void
46 | {
47 | $email = $this->email;
48 |
49 | // Extract comments while preserving their positions
50 | $email = $this->extractComments($email);
51 |
52 | // Split on the last @ symbol
53 | $atPos = strrpos($email, '@');
54 | if ($atPos === false) {
55 | return;
56 | }
57 |
58 | $this->localPart = substr($email, 0, $atPos);
59 | $this->domain = substr($email, $atPos + 1);
60 | }
61 |
62 | /**
63 | * Extracts comments from an email address while preserving their positions
64 | *
65 | * @param string $email The email address to process
66 | * @return string The email address with comments removed
67 | */
68 | private function extractComments(string $email): string
69 | {
70 | $result = '';
71 | $inComment = false;
72 | $commentDepth = 0;
73 | $currentComment = '';
74 | $escaped = false;
75 |
76 | for ($i = 0, $iMax = strlen($email); $i < $iMax; $i++) {
77 | $char = $email[$i];
78 |
79 | if ($escaped) {
80 | if ($inComment) {
81 | $currentComment .= $char;
82 | } else {
83 | $result .= $char;
84 | }
85 | $escaped = false;
86 | continue;
87 | }
88 |
89 | if ($char === '\\') {
90 | $escaped = true;
91 | if ($inComment) {
92 | $currentComment .= $char;
93 | } else {
94 | $result .= $char;
95 | }
96 | continue;
97 | }
98 |
99 | if ($char === '(') {
100 | if ($inComment) {
101 | $commentDepth++;
102 | $currentComment .= $char;
103 | } else {
104 | $inComment = true;
105 | $commentDepth = 1;
106 | }
107 | continue;
108 | }
109 |
110 | if ($char === ')') {
111 | if ($inComment) {
112 | $commentDepth--;
113 | if ($commentDepth === 0) {
114 | $this->comments[] = $currentComment;
115 | $currentComment = '';
116 | $inComment = false;
117 | } else {
118 | $currentComment .= $char;
119 | }
120 | } else {
121 | $result .= $char;
122 | }
123 | continue;
124 | }
125 |
126 | if ($inComment) {
127 | $currentComment .= $char;
128 | } else {
129 | $result .= $char;
130 | }
131 | }
132 |
133 | return $result;
134 | }
135 |
136 | /**
137 | * Returns the domain name portion of the email address.
138 | *
139 | * @return string|null
140 | */
141 | public function getDomain(): ?string
142 | {
143 | return $this->domain;
144 | }
145 |
146 | /**
147 | * Returns the local part of the email address.
148 | *
149 | * @return string|null
150 | */
151 | public function getLocalPart(): ?string
152 | {
153 | return $this->localPart;
154 | }
155 |
156 | /**
157 | * Returns the complete email address.
158 | *
159 | * @return string
160 | */
161 | public function getEmailAddress(): string
162 | {
163 | return $this->email;
164 | }
165 |
166 | /**
167 | * Returns any comments found in the email address.
168 | *
169 | * @return array
170 | */
171 | public function getComments(): array
172 | {
173 | return $this->comments;
174 | }
175 |
176 | /**
177 | * Returns the username portion of the email address.
178 | *
179 | * @since 1.1.0
180 | * @return string
181 | */
182 | private function getUsername(): string
183 | {
184 | return $this->localPart ?? '';
185 | }
186 |
187 | /**
188 | * Determines if a gmail account is using the "plus trick".
189 | *
190 | * @since 1.1.0
191 | * @return bool
192 | */
193 | public function isGmailWithPlusChar(): bool
194 | {
195 | $result = false;
196 | if (in_array($this->getDomain(), ['gmail.com', 'googlemail.com'])) {
197 | $result = strpos($this->getUsername(), '+') !== false;
198 | }
199 |
200 | return $result;
201 | }
202 |
203 | /**
204 | * Returns a gmail address without the "plus trick" portion of the email address.
205 | *
206 | * @since 1.1.0
207 | * @return string
208 | */
209 | public function getGmailAddressWithoutPlus(): string
210 | {
211 | return preg_replace('/^(.+?)(\+.+?)(@.+)/', '$1$3', $this->getEmailAddress());
212 | }
213 |
214 | /**
215 | * Returns a gmail address without the "plus trick" portion of the email address and all dots removed.
216 | *
217 | * @since 1.1.4
218 | * @return string
219 | */
220 | public function getSanitizedGmailAddress(): string
221 | {
222 | $email = new EmailAddress($this->getGmailAddressWithoutPlus());
223 | return sprintf(
224 | '%s@%s',
225 | str_replace('.', '', $email->getUsername()),
226 | $email->getDomain()
227 | );
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/EmailValidator/EmailValidator.php:
--------------------------------------------------------------------------------
1 |
71 | * @since 2.0.0
72 | */
73 | private array $customValidators = [];
74 |
75 | /**
76 | * @var int
77 | */
78 | private int $reason;
79 |
80 | /**
81 | * @var EmailAddress|null
82 | * @since 1.1.0
83 | */
84 | private ?EmailAddress $emailAddress = null;
85 |
86 | public function __construct(array $config = [])
87 | {
88 | $this->reason = self::NO_ERROR;
89 |
90 | $policy = new Policy($config);
91 |
92 | $this->mxValidator = new MxValidator($policy);
93 | $this->basicValidator = new BasicValidator($policy);
94 | $this->rfc5322Validator = new Rfc5322Validator($policy);
95 | $this->bannedListValidator = new BannedListValidator($policy);
96 | $this->disposableEmailValidator = new DisposableEmailValidator($policy);
97 | $this->freeEmailValidator = new FreeEmailValidator($policy);
98 | $this->gmailValidator = new GmailValidator($policy);
99 | }
100 |
101 | /**
102 | * Register a custom validator
103 | *
104 | * @param AValidator $validator
105 | * @return void
106 | * @since 2.0.0
107 | */
108 | public function registerValidator(AValidator $validator): void
109 | {
110 | $this->customValidators[] = $validator;
111 | }
112 |
113 | /**
114 | * Validate an email address by the rules set forth in the Policy
115 | *
116 | * @param string $email
117 | * @return bool
118 | */
119 | public function validate(string $email): bool
120 | {
121 | $this->resetErrorCode();
122 |
123 | $this->emailAddress = new EmailAddress($email);
124 |
125 | if (!$this->basicValidator->validate($this->emailAddress)) {
126 | $this->reason = self::FAIL_BASIC;
127 | } elseif (!$this->rfc5322Validator->validate($this->emailAddress)) {
128 | $this->reason = self::FAIL_RFC5322;
129 | } elseif (!$this->mxValidator->validate($this->emailAddress)) {
130 | $this->reason = self::FAIL_MX_RECORD;
131 | } elseif (!$this->bannedListValidator->validate($this->emailAddress)) {
132 | $this->reason = self::FAIL_BANNED_DOMAIN;
133 | } elseif (!$this->disposableEmailValidator->validate($this->emailAddress)) {
134 | $this->reason = self::FAIL_DISPOSABLE_DOMAIN;
135 | } elseif (!$this->freeEmailValidator->validate($this->emailAddress)) {
136 | $this->reason = self::FAIL_FREE_PROVIDER;
137 | } else {
138 | foreach ($this->customValidators as $validator) {
139 | if (!$validator->validate($this->emailAddress)) {
140 | $this->reason = self::FAIL_CUSTOM;
141 | break;
142 | }
143 | }
144 | }
145 |
146 | return $this->reason === self::NO_ERROR;
147 | }
148 |
149 | /**
150 | * Returns the error code constant value for invalid email addresses.
151 | *
152 | * For use by integrating systems to create their own error messages.
153 | *
154 | * @since 1.0.1
155 | * @return int
156 | */
157 | public function getErrorCode(): int
158 | {
159 | return $this->reason;
160 | }
161 |
162 | /**
163 | * Returns an error message for invalid email addresses
164 | *
165 | * @return string
166 | */
167 | public function getErrorReason(): string
168 | {
169 | switch ($this->reason) {
170 | case self::FAIL_BASIC:
171 | $msg = 'Invalid format';
172 | break;
173 | case self::FAIL_RFC5322:
174 | $msg = 'Does not comply with RFC 5322';
175 | break;
176 | case self::FAIL_MX_RECORD:
177 | $msg = 'Domain does not accept email';
178 | break;
179 | case self::FAIL_BANNED_DOMAIN:
180 | $msg = 'Domain is banned';
181 | break;
182 | case self::FAIL_DISPOSABLE_DOMAIN:
183 | $msg = 'Domain is used by disposable email providers';
184 | break;
185 | case self::FAIL_FREE_PROVIDER:
186 | $msg = 'Domain is used by free email providers';
187 | break;
188 | case self::FAIL_CUSTOM:
189 | $msg = 'Failed custom validation';
190 | break;
191 | case self::NO_ERROR:
192 | default:
193 | $msg = '';
194 | }
195 |
196 | return $msg;
197 | }
198 |
199 | /**
200 | * Resets the error code so each validation starts off defaulting to "valid"
201 | *
202 | * @since 1.0.2
203 | * @return void
204 | */
205 | private function resetErrorCode(): void
206 | {
207 | $this->reason = self::NO_ERROR;
208 | }
209 |
210 | /**
211 | * Determines if a gmail account is using the "plus trick".
212 | *
213 | * @codeCoverageIgnore
214 | * @since 1.1.0
215 | * @return bool
216 | */
217 | public function isGmailWithPlusChar(): bool
218 | {
219 | return $this->emailAddress !== null && $this->gmailValidator->isGmailWithPlusChar($this->emailAddress);
220 | }
221 |
222 | /**
223 | * Returns a gmail address with the "plus trick" portion of the email address.
224 | *
225 | * @codeCoverageIgnore
226 | * @since 1.1.0
227 | * @return string
228 | */
229 | public function getGmailAddressWithoutPlus(): string
230 | {
231 | if ($this->emailAddress === null) {
232 | return '';
233 | }
234 | return $this->gmailValidator->getGmailAddressWithoutPlus($this->emailAddress);
235 | }
236 |
237 | /**
238 | * Returns a sanitized gmail address (plus trick removed and dots removed).
239 | *
240 | * @codeCoverageIgnore
241 | * @since 1.1.4
242 | * @return string
243 | */
244 | public function getSanitizedGmailAddress(): string
245 | {
246 | if ($this->emailAddress === null) {
247 | return '';
248 | }
249 | return $this->gmailValidator->getSanitizedGmailAddress($this->emailAddress);
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/src/EmailValidator/Policy.php:
--------------------------------------------------------------------------------
1 |
26 | */
27 | private array $bannedList;
28 |
29 | /**
30 | * @var bool
31 | */
32 | private bool $checkMxRecords;
33 |
34 | /**
35 | * @var array
36 | */
37 | private array $disposableList;
38 |
39 | /**
40 | * @var array
41 | */
42 | private array $freeList;
43 |
44 | /**
45 | * @var bool
46 | */
47 | private bool $localDisposableOnly;
48 |
49 | /**
50 | * @var bool
51 | */
52 | private bool $localFreeOnly;
53 |
54 | public function __construct(array $config = [])
55 | {
56 | $this->checkMxRecords = (bool) ($config['checkMxRecords'] ?? true);
57 | $this->checkBannedListedEmail = (bool) ($config['checkBannedListedEmail'] ?? false);
58 | $this->checkDisposableEmail = (bool) ($config['checkDisposableEmail'] ?? false);
59 | $this->checkFreeEmail = (bool) ($config['checkFreeEmail'] ?? false);
60 | $this->localDisposableOnly = (bool) ($config['LocalDisposableOnly'] ?? false);
61 | $this->localFreeOnly = (bool) ($config['LocalFreeOnly'] ?? false);
62 |
63 | $this->bannedList = $config['bannedList'] ?? [];
64 | $this->disposableList = $config['disposableList'] ?? [];
65 | $this->freeList = $config['freeList'] ?? [];
66 | }
67 |
68 | /**
69 | * Validate MX records?
70 | *
71 | * @return bool
72 | */
73 | public function validateMxRecord(): bool
74 | {
75 | return $this->checkMxRecords;
76 | }
77 |
78 | /**
79 | * Check domain if it is on the banned list?
80 | *
81 | * @return bool
82 | */
83 | public function checkBannedListedEmail(): bool
84 | {
85 | return $this->checkBannedListedEmail;
86 | }
87 |
88 | /**
89 | * Check if the domain is used by a disposable email site?
90 | *
91 | * @return bool
92 | */
93 | public function checkDisposableEmail(): bool
94 | {
95 | return $this->checkDisposableEmail;
96 | }
97 |
98 | /**
99 | * Check if the domain is used by a free email site?
100 | *
101 | * @return bool
102 | */
103 | public function checkFreeEmail(): bool
104 | {
105 | return $this->checkFreeEmail;
106 | }
107 |
108 | /**
109 | * Check if only a local copy of disposable email address domains should be used. Saves the overhead of
110 | * making HTTP requests to get the list the first time that validation is called.
111 | *
112 | * @return bool
113 | */
114 | public function checkDisposableLocalListOnly(): bool
115 | {
116 | return $this->localDisposableOnly;
117 | }
118 |
119 | /**
120 | * Check if only a local copy of free email address domains should be used. Saves the overhead of
121 | * making HTTP requests to get the list the first time that validation is called.
122 | *
123 | * @return bool
124 | */
125 | public function checkFreeLocalListOnly(): bool
126 | {
127 | return $this->localFreeOnly;
128 | }
129 |
130 | /**
131 | * Returns the list of banned domains.
132 | *
133 | * @return array
134 | */
135 | public function getBannedList(): array
136 | {
137 | return $this->bannedList;
138 | }
139 |
140 | /**
141 | * Returns the list of free email provider domains.
142 | *
143 | * @return array
144 | */
145 | public function getFreeList(): array
146 | {
147 | return $this->freeList;
148 | }
149 |
150 | /**
151 | * Returns the list of disposable email domains.
152 | *
153 | * @return array
154 | */
155 | public function getDisposableList(): array
156 | {
157 | return $this->disposableList;
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/AProviderValidator.php:
--------------------------------------------------------------------------------
1 |
20 | */
21 | protected static array $providers = [];
22 |
23 | /**
24 | * Gets and merges provider lists from various sources
25 | *
26 | * Fetches public lists of provider domains and merges them together into one array.
27 | * If a custom list is provided, it is merged into the new list.
28 | *
29 | * @param bool $checkLocalOnly If true, only use the provided list and skip external sources
30 | * @param array $list Custom list of provider domains to merge with external lists
31 | * @return array Merged and deduplicated list of provider domains
32 | */
33 | public function getList(bool $checkLocalOnly = false, array $list = []): array
34 | {
35 | $providers = [];
36 | if (!$checkLocalOnly) {
37 | foreach (static::$providers as $provider) {
38 | if (filter_var($provider['url'], FILTER_VALIDATE_URL)) {
39 | $content = @file_get_contents($provider['url']);
40 | if ($content) {
41 | $providers[] = $this->getExternalList($content, $provider['format']);
42 | }
43 | }
44 | }
45 | }
46 | return array_values(array_filter(array_unique(array_merge($list, ...$providers)), 'is_string'));
47 | }
48 |
49 | /**
50 | * Parses a provider list based on its format
51 | *
52 | * Supports JSON and plain text formats for provider lists.
53 | *
54 | * @param string $content The content of the provider list
55 | * @param string $type The format of the list ('json' or 'txt')
56 | * @return array Parsed list of provider domains
57 | */
58 | protected function getExternalList(string $content, string $type): array
59 | {
60 | if (empty($content)) {
61 | return [];
62 | }
63 |
64 | switch ($type) {
65 | case 'json':
66 | $providers = json_decode($content, true);
67 | if (!is_array($providers)) {
68 | return [];
69 | }
70 | break;
71 | case 'txt':
72 | default:
73 | $providers = array_filter(explode("\n", str_replace("\r\n", "\n", $content)), 'strlen');
74 | break;
75 | }
76 | return array_values(array_filter($providers, 'is_string'));
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/AValidator.php:
--------------------------------------------------------------------------------
1 | policy = $policy;
33 | }
34 |
35 | /**
36 | * Validates an email address according to specific rules
37 | *
38 | * @param EmailAddress $email The email address to validate
39 | * @return bool True if the email address passes validation, false otherwise
40 | */
41 | abstract public function validate(EmailAddress $email): bool;
42 | }
43 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/BannedListValidator.php:
--------------------------------------------------------------------------------
1 | policy->checkBannedListedEmail()) {
27 | $domain = $email->getDomain();
28 | foreach ($this->policy->getBannedList() as $bannedDomain) {
29 | if (fnmatch($bannedDomain, $domain ?? '')) {
30 | return false;
31 | }
32 | }
33 | }
34 | return true;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/BasicValidator.php:
--------------------------------------------------------------------------------
1 | getEmailAddress(), FILTER_VALIDATE_EMAIL);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/DisposableEmailValidator.php:
--------------------------------------------------------------------------------
1 | Array of client-provided disposable email providers
22 | */
23 | protected array $disposableEmailListProviders = [];
24 |
25 | /**
26 | * Array of URLs containing lists of disposable email addresses and their formats
27 | *
28 | * @var array Array of URLs containing a list of disposable email addresses and the format of that list
29 | */
30 | protected static array $providers = [
31 | [
32 | 'format' => 'txt',
33 | 'url' => 'https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blocklist.conf'
34 | ],
35 | [
36 | 'format' => 'json',
37 | 'url' => 'https://raw.githubusercontent.com/ivolo/disposable-email-domains/master/wildcard.json'
38 | ],
39 | ];
40 |
41 | /**
42 | * Validates an email address against known disposable email providers
43 | *
44 | * Checks if validating against disposable domains is enabled. If so, gets the list of disposable domains
45 | * and checks if the domain is one of them.
46 | *
47 | * @param EmailAddress $email The email address to validate
48 | * @return bool True if the domain is not a disposable email provider or validation is disabled, false if it is a disposable provider
49 | */
50 | public function validate(EmailAddress $email): bool
51 | {
52 | if (!$this->policy->checkDisposableEmail()) {
53 | return true;
54 | }
55 |
56 | if ($this->disposableEmailListProviders === []) {
57 | $this->disposableEmailListProviders = $this->getList(
58 | $this->policy->checkDisposableLocalListOnly(),
59 | $this->policy->getDisposableList()
60 | );
61 | }
62 |
63 | $domain = $email->getDomain();
64 | if ($domain === null) {
65 | return true;
66 | }
67 |
68 | return !in_array($domain, $this->disposableEmailListProviders, true);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/Domain/DomainLiteralValidator.php:
--------------------------------------------------------------------------------
1 | hasValidBrackets($domain)) {
22 | return false;
23 | }
24 |
25 | // Remove brackets for content validation
26 | $content = substr($domain, 1, -1);
27 |
28 | // Empty domain literals are invalid
29 | if ($content === '') {
30 | return false;
31 | }
32 |
33 | // Check for whitespace or control characters
34 | if ($this->hasInvalidCharacters($content)) {
35 | return false;
36 | }
37 |
38 | // Try IPv4 first, then IPv6
39 | return $this->validateIPv4($content) || $this->validateIPv6($content);
40 | }
41 |
42 | /**
43 | * Checks if a domain literal has valid opening and closing brackets
44 | *
45 | * @param string $domain The domain literal to validate
46 | * @return bool True if the brackets are valid
47 | */
48 | private function hasValidBrackets(string $domain): bool
49 | {
50 | return substr($domain, 0, 1) === '[' && substr($domain, -1) === ']';
51 | }
52 |
53 | /**
54 | * Checks for whitespace or control characters
55 | *
56 | * @param string $content The content to check
57 | * @return bool True if invalid characters are found
58 | */
59 | private function hasInvalidCharacters(string $content): bool
60 | {
61 | return (bool)preg_match('/[\s\x00-\x1F\x7F]/', $content);
62 | }
63 |
64 | /**
65 | * Validates an IPv4 address
66 | *
67 | * @param string $address The IPv4 address to validate
68 | * @return bool True if the IPv4 address is valid
69 | */
70 | private function validateIPv4(string $address): bool
71 | {
72 | // Split into octets
73 | $octets = explode('.', $address);
74 |
75 | // Must have exactly 4 octets
76 | if (count($octets) !== 4) {
77 | return false;
78 | }
79 |
80 | // Validate each octet
81 | foreach ($octets as $octet) {
82 | if (!$this->validateIPv4Octet($octet)) {
83 | return false;
84 | }
85 | }
86 |
87 | return true;
88 | }
89 |
90 | /**
91 | * Validates a single IPv4 octet
92 | *
93 | * @param string $octet The octet to validate
94 | * @return bool True if the octet is valid
95 | */
96 | private function validateIPv4Octet(string $octet): bool
97 | {
98 | // Empty octets are invalid
99 | if ($octet === '') {
100 | return false;
101 | }
102 |
103 | // Must be numeric and in valid range
104 | if (!is_numeric($octet)) {
105 | return false;
106 | }
107 |
108 | $value = (int)$octet;
109 | return $value >= 0 && $value <= 255;
110 | }
111 |
112 | /**
113 | * Validates an IPv6 address
114 | *
115 | * @param string $address The IPv6 address to validate
116 | * @return bool True if the IPv6 address is valid
117 | */
118 | private function validateIPv6(string $address): bool
119 | {
120 | // Must start with 'IPv6:' (case-sensitive)
121 | if (substr($address, 0, 5) !== 'IPv6:') {
122 | return false;
123 | }
124 |
125 | // Remove prefix
126 | $address = substr($address, 5);
127 |
128 | // Handle compressed notation
129 | if (strpos($address, '::') !== false) {
130 | return $this->validateCompressedIPv6($address);
131 | }
132 |
133 | // Split into groups
134 | $groups = explode(':', $address);
135 |
136 | // Must have exactly 8 groups for uncompressed notation
137 | if (count($groups) !== 8) {
138 | return false;
139 | }
140 |
141 | // Validate each group
142 | foreach ($groups as $group) {
143 | if (!$this->validateIPv6Group($group)) {
144 | return false;
145 | }
146 | }
147 |
148 | return true;
149 | }
150 |
151 | /**
152 | * Validates a compressed IPv6 address
153 | *
154 | * @param string $address The IPv6 address to validate (without prefix)
155 | * @return bool True if the IPv6 address is valid
156 | */
157 | private function validateCompressedIPv6(string $address): bool
158 | {
159 | // Only one :: allowed
160 | if (substr_count($address, '::') > 1) {
161 | return false;
162 | }
163 |
164 | // Split on ::
165 | $parts = explode('::', $address);
166 | if (count($parts) !== 2) {
167 | return false;
168 | }
169 |
170 | // Split each part into groups
171 | $leftGroups = $parts[0] ? explode(':', $parts[0]) : [];
172 | $rightGroups = $parts[1] ? explode(':', $parts[1]) : [];
173 |
174 | // Calculate total groups
175 | $totalGroups = count($leftGroups) + count($rightGroups);
176 | if ($totalGroups >= 8) {
177 | return false;
178 | }
179 |
180 | // Validate left groups
181 | foreach ($leftGroups as $group) {
182 | if (!$this->validateIPv6Group($group)) {
183 | return false;
184 | }
185 | }
186 |
187 | // Validate right groups
188 | foreach ($rightGroups as $group) {
189 | if (!$this->validateIPv6Group($group)) {
190 | return false;
191 | }
192 | }
193 |
194 | return true;
195 | }
196 |
197 | /**
198 | * Validates a single IPv6 group
199 | *
200 | * @param string $group The group to validate
201 | * @return bool True if the group is valid
202 | */
203 | private function validateIPv6Group(string $group): bool
204 | {
205 | // Empty groups are invalid
206 | if ($group === '') {
207 | return false;
208 | }
209 |
210 | // Must be 1-4 hexadecimal digits (case-insensitive)
211 | return (bool)preg_match('/^[0-9A-Fa-f]{1,4}$/', $group);
212 | }
213 | }
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/Domain/DomainNameValidator.php:
--------------------------------------------------------------------------------
1 | validateLength($domain)) {
30 | return false;
31 | }
32 |
33 | // Split into labels
34 | $labels = explode('.', $domain);
35 |
36 | // Must have at least two labels
37 | if (count($labels) < 2) {
38 | return false;
39 | }
40 |
41 | // Validate each label
42 | foreach ($labels as $label) {
43 | if (!$this->validateLabel($label)) {
44 | return false;
45 | }
46 | }
47 |
48 | return true;
49 | }
50 |
51 | /**
52 | * Validates the length of a domain
53 | *
54 | * @param string $domain The domain to validate
55 | * @return bool True if the length is valid
56 | */
57 | private function validateLength(string $domain): bool
58 | {
59 | return strlen($domain) <= self::MAX_DOMAIN_LENGTH;
60 | }
61 |
62 | /**
63 | * Validates a single domain label
64 | *
65 | * @param string $label The domain label to validate
66 | * @return bool True if the domain label is valid
67 | */
68 | private function validateLabel(string $label): bool
69 | {
70 | // Check length
71 | if (!$this->validateLabelLength($label)) {
72 | return false;
73 | }
74 |
75 | // Handle IDN labels (starting with 'xn--')
76 | if (substr($label, 0, 4) === 'xn--') {
77 | return $this->validateIDNLabel($label);
78 | }
79 |
80 | // Single character labels are allowed if they're alphanumeric
81 | if (strlen($label) === 1) {
82 | return ctype_alnum($label);
83 | }
84 |
85 | // Must start and end with alphanumeric
86 | if (!$this->hasValidLabelBoundaries($label)) {
87 | return false;
88 | }
89 |
90 | // Check for valid characters and format
91 | if (!$this->hasValidLabelFormat($label)) {
92 | return false;
93 | }
94 |
95 | // Check for consecutive hyphens
96 | return !$this->hasConsecutiveHyphens($label);
97 | }
98 |
99 | /**
100 | * Validates the length of a domain label
101 | *
102 | * @param string $label The domain label to validate
103 | * @return bool True if the length is valid
104 | */
105 | private function validateLabelLength(string $label): bool
106 | {
107 | return strlen($label) <= self::MAX_DOMAIN_LABEL_LENGTH && $label !== '';
108 | }
109 |
110 | /**
111 | * Validates an IDN (Internationalized Domain Name) label
112 | *
113 | * @param string $label The IDN label to validate
114 | * @return bool True if the IDN label is valid
115 | */
116 | private function validateIDNLabel(string $label): bool
117 | {
118 | // Must be at least 5 characters (xn-- plus at least one character)
119 | if (strlen($label) < 5) {
120 | return false;
121 | }
122 |
123 | // Rest of the label must be alphanumeric or hyphen
124 | $rest = substr($label, 4);
125 | return (bool)preg_match('/^[a-zA-Z0-9-]+$/', $rest);
126 | }
127 |
128 | /**
129 | * Checks if a domain label has valid start and end characters
130 | *
131 | * @param string $label The domain label to validate
132 | * @return bool True if the boundaries are valid
133 | */
134 | private function hasValidLabelBoundaries(string $label): bool
135 | {
136 | return ctype_alnum($label[0]) && ctype_alnum(substr($label, -1));
137 | }
138 |
139 | /**
140 | * Checks if a domain label has valid format
141 | *
142 | * @param string $label The domain label to validate
143 | * @return bool True if the format is valid
144 | */
145 | private function hasValidLabelFormat(string $label): bool
146 | {
147 | return (bool)preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/', $label);
148 | }
149 |
150 | /**
151 | * Checks if a domain label has consecutive hyphens
152 | *
153 | * @param string $label The domain label to validate
154 | * @return bool True if the label has consecutive hyphens
155 | */
156 | private function hasConsecutiveHyphens(string $label): bool
157 | {
158 | return strpos($label, '--') !== false;
159 | }
160 | }
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/FreeEmailValidator.php:
--------------------------------------------------------------------------------
1 | Array of client-provided free email providers
22 | */
23 | protected array $freeEmailListProviders = [];
24 |
25 | /**
26 | * Array of URLs containing lists of free email addresses and their formats
27 | *
28 | * @var array Array of URLs containing a list of free email addresses and the format of that list
29 | */
30 | protected static array $providers = [
31 | [
32 | 'format' => 'txt',
33 | 'url' => 'https://gist.githubusercontent.com/tbrianjones/5992856/raw/93213efb652749e226e69884d6c048e595c1280a/free_email_provider_domains.txt'
34 | ],
35 | ];
36 |
37 | /**
38 | * Validates an email address against known free email providers
39 | *
40 | * Checks if validating against free email domains is enabled. If so, gets the list of email domains
41 | * and checks if the domain is one of them.
42 | *
43 | * @param EmailAddress $email The email address to validate
44 | * @return bool True if the domain is not a free email provider or validation is disabled, false if it is a free provider
45 | */
46 | public function validate(EmailAddress $email): bool
47 | {
48 | if (!$this->policy->checkFreeEmail()) {
49 | return true;
50 | }
51 |
52 | if ($this->freeEmailListProviders === []) {
53 | $this->freeEmailListProviders = $this->getList(
54 | $this->policy->checkFreeLocalListOnly(),
55 | $this->policy->getFreeList()
56 | );
57 | }
58 |
59 | $domain = $email->getDomain();
60 | if ($domain === null) {
61 | return true;
62 | }
63 |
64 | return !in_array($domain, $this->freeEmailListProviders, true);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/GmailValidator.php:
--------------------------------------------------------------------------------
1 | getDomain();
32 | return $domain !== null && in_array($domain, ['gmail.com', 'googlemail.com'], true);
33 | }
34 |
35 | /**
36 | * Determines if a Gmail account is using the "plus trick"
37 | *
38 | * @param EmailAddress $email The email address to check
39 | * @return bool True if the Gmail address uses the plus trick, false otherwise
40 | * @since 2.0.0
41 | */
42 | public function isGmailWithPlusChar(EmailAddress $email): bool
43 | {
44 | return $this->validate($email) && $email->isGmailWithPlusChar();
45 | }
46 |
47 | /**
48 | * Returns a Gmail address with the "plus trick" portion removed
49 | *
50 | * @param EmailAddress $email The email address to transform
51 | * @return string The Gmail address without the plus trick portion
52 | * @since 2.0.0
53 | */
54 | public function getGmailAddressWithoutPlus(EmailAddress $email): string
55 | {
56 | return $this->validate($email) ? $email->getGmailAddressWithoutPlus() : $email->getEmailAddress();
57 | }
58 |
59 | /**
60 | * Returns a sanitized Gmail address (plus trick removed and dots removed)
61 | *
62 | * @param EmailAddress $email The email address to sanitize
63 | * @return string The sanitized Gmail address
64 | * @since 2.0.0
65 | */
66 | public function getSanitizedGmailAddress(EmailAddress $email): string
67 | {
68 | return $this->validate($email) ? $email->getSanitizedGmailAddress() : $email->getEmailAddress();
69 | }
70 | }
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/IValidator.php:
--------------------------------------------------------------------------------
1 | validateAtom($atom)) {
34 | return false;
35 | }
36 | }
37 |
38 | return true;
39 | }
40 |
41 | /**
42 | * Validates a single atom
43 | *
44 | * @param string $atom The atom to validate
45 | * @return bool True if the atom is valid
46 | */
47 | private function validateAtom(string $atom): bool
48 | {
49 | if ($atom === '') {
50 | return false;
51 | }
52 |
53 | // Check for valid characters
54 | return (bool)preg_match('/^[a-zA-Z0-9!#$%&\'*+\-\/=?^_`{|}~]+$/', $atom);
55 | }
56 | }
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/LocalPart/QuotedStringValidator.php:
--------------------------------------------------------------------------------
1 | hasValidQuotes($localPart)) {
27 | return false;
28 | }
29 |
30 | // Remove outer quotes for content validation
31 | $content = substr($localPart, 1, -1);
32 |
33 | // Empty quoted strings are valid
34 | if ($content === '') {
35 | return true;
36 | }
37 |
38 | return $this->validateContent($content);
39 | }
40 |
41 | /**
42 | * Checks if a quoted string has valid opening and closing quotes
43 | *
44 | * @param string $localPart The quoted string to validate
45 | * @return bool True if the quotes are valid
46 | */
47 | private function hasValidQuotes(string $localPart): bool
48 | {
49 | return substr($localPart, 0, 1) === '"' && substr($localPart, -1) === '"';
50 | }
51 |
52 | /**
53 | * Validates the content of a quoted string
54 | *
55 | * @param string $content The content to validate (without outer quotes)
56 | * @return bool True if the content is valid
57 | */
58 | private function validateContent(string $content): bool
59 | {
60 | $length = strlen($content);
61 | $i = 0;
62 |
63 | while ($i < $length) {
64 | $char = $content[$i];
65 | $charCode = ord($char);
66 |
67 | // Handle backslash escapes
68 | if ($char === '\\') {
69 | // Can't end with a lone backslash
70 | if ($i === $length - 1) {
71 | return false;
72 | }
73 | // Next character must be either a quote or backslash
74 | $nextChar = $content[$i + 1];
75 | if (!in_array($nextChar, self::MUST_ESCAPE, true)) {
76 | return false;
77 | }
78 | // Skip the escaped character
79 | $i += 2;
80 | continue;
81 | }
82 |
83 | // Non-printable characters are never allowed (except tab)
84 | if (($charCode < 32 || $charCode > 126) && $charCode !== 9) {
85 | return false;
86 | }
87 |
88 | // Unescaped quotes are not allowed
89 | if ($char === '"') {
90 | return false;
91 | }
92 |
93 | $i++;
94 | }
95 |
96 | return true;
97 | }
98 | }
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/MxValidator.php:
--------------------------------------------------------------------------------
1 | policy->validateMxRecord()) {
28 | $domain = sprintf('%s.', $email->getDomain());
29 | $valid = checkdnsrr(idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46), 'MX');
30 | }
31 | return $valid;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/EmailValidator/Validator/Rfc5322Validator.php:
--------------------------------------------------------------------------------
1 | atomValidator = new AtomValidator();
31 | $this->quotedStringValidator = new QuotedStringValidator();
32 | $this->domainNameValidator = new DomainNameValidator();
33 | $this->domainLiteralValidator = new DomainLiteralValidator();
34 | }
35 |
36 | /**
37 | * Validates an email address according to RFC 5322
38 | *
39 | * @param EmailAddress $email The email address to validate
40 | * @return bool True if the email address is valid
41 | */
42 | public function validate(EmailAddress $email): bool
43 | {
44 | $localPart = $email->getLocalPart();
45 | $domain = $email->getDomain();
46 |
47 | if ($localPart === null || $domain === null) {
48 | return false;
49 | }
50 |
51 | return $this->validateLocalPart($localPart) && $this->validateDomain($domain);
52 | }
53 |
54 | /**
55 | * Validates the local part of an email address
56 | *
57 | * @param string $localPart The local part to validate
58 | * @return bool True if the local part is valid
59 | */
60 | private function validateLocalPart(string $localPart): bool
61 | {
62 | // Empty local part is invalid
63 | if ($localPart === '') {
64 | return false;
65 | }
66 |
67 | // Check length
68 | if (strlen($localPart) > self::MAX_LOCAL_PART_LENGTH) {
69 | return false;
70 | }
71 |
72 | // Check if it's a quoted string
73 | if (substr($localPart, 0, 1) === '"') {
74 | return $this->quotedStringValidator->validate($localPart);
75 | }
76 |
77 | // Otherwise, treat as dot-atom
78 | return $this->atomValidator->validate($localPart);
79 | }
80 |
81 | /**
82 | * Validates the domain part of an email address
83 | *
84 | * @param string $domain The domain to validate
85 | * @return bool True if the domain is valid
86 | */
87 | private function validateDomain(string $domain): bool
88 | {
89 | // Empty domain is invalid
90 | if ($domain === '') {
91 | return false;
92 | }
93 |
94 | // Check if it's a domain literal
95 | if (substr($domain, 0, 1) === '[') {
96 | return $this->domainLiteralValidator->validate($domain);
97 | }
98 |
99 | // Otherwise, treat as domain name
100 | return $this->domainNameValidator->validate($domain);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/tests/EmailValidator/EmailAddressTest.php:
--------------------------------------------------------------------------------
1 | getDomain(), $domain);
27 | }
28 |
29 | public function emailDataProvider(): array
30 | {
31 | return [
32 | ['test@johnconde.net'],
33 | ['test@gmail.com'],
34 | ['test@hotmail.com'],
35 | ['test@outlook.com'],
36 | ['test@yahoo.com'],
37 | ['test@domain.com'],
38 | ['test@example.com'],
39 | ['test@example2.com'],
40 | ['test@nobugmail.com'],
41 | ['test@mxfuel.com'],
42 | ['test@cellurl.com'],
43 | ['test@10minutemail.com'],
44 | ];
45 | }
46 |
47 | /**
48 | * @dataProvider emailDataProvider
49 | * @param string $emailAddress
50 | */
51 | public function testGetEmailAddress(string $emailAddress): void
52 | {
53 | $email = new EmailAddress($emailAddress);
54 | self::assertEquals($email->getEmailAddress(), $emailAddress);
55 | }
56 |
57 | public function emailWithPlusDataProvider(): array
58 | {
59 | return [
60 | ['test@gmail.com', false],
61 | ['test+example@gmail.com', true],
62 | ['test@example.com', false],
63 | ['test+example@example.com', false],
64 | ['@example.com', false],
65 | ];
66 | }
67 |
68 | /**
69 | * @dataProvider emailWithPlusDataProvider
70 | * @param string $emailAddress
71 | * @param bool $hasPlus
72 | */
73 | public function testIsGmailWithPlusChar(string $emailAddress, bool $hasPlus): void
74 | {
75 | $email = new EmailAddress($emailAddress);
76 | self::assertEquals($hasPlus, $email->isGmailWithPlusChar());
77 | }
78 |
79 | public function testGetGmailAddressWithoutPlus(): void
80 | {
81 | $email = new EmailAddress('user+test@example.com');
82 | self::assertEquals('user@example.com', $email->getGmailAddressWithoutPlus());
83 | }
84 |
85 | public function gmailAddressDataProvider(): array
86 | {
87 | return [
88 | ['example.com@gmail.com' , 'examplecom@gmail.com'],
89 | ['example.com@googlemail.com' , 'examplecom@googlemail.com'],
90 | ['e.x.a.m.p.l.e.c.o.m@gmail.com' , 'examplecom@gmail.com'],
91 | ['e.x.a.m.p.l.e.c.o.m+string@gmail.com' , 'examplecom@gmail.com'],
92 | ['example.com+string@gmail.com' , 'examplecom@gmail.com'],
93 | ];
94 | }
95 |
96 | /**
97 | * @dataProvider gmailAddressDataProvider
98 | * @param string $emailAddress
99 | * @param string $resultAddress
100 | */
101 | public function testGetSanitizedGmailAddress(string $emailAddress, string $resultAddress): void
102 | {
103 | $email = new EmailAddress($emailAddress);
104 | self::assertEquals($email->getSanitizedGmailAddress(), $resultAddress);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/EmailValidator/EmailValidatorTest.php:
--------------------------------------------------------------------------------
1 | createMock(BasicValidator::class);
48 | $basicValidator->method('validate')
49 | ->willReturn($basic);
50 | $bValidator = new \ReflectionProperty($emailValidator, 'basicValidator');
51 | $bValidator->setAccessible(true);
52 | $bValidator->setValue($emailValidator, $basicValidator);
53 |
54 | $rfc5322Validator = $this->createMock(Rfc5322Validator::class);
55 | $rfc5322Validator->method('validate')
56 | ->willReturn($rfc5322);
57 | $rValidator = new \ReflectionProperty($emailValidator, 'rfc5322Validator');
58 | $rValidator->setAccessible(true);
59 | $rValidator->setValue($emailValidator, $rfc5322Validator);
60 |
61 | $mxValidator = $this->createMock(MxValidator::class);
62 | $mxValidator->method('validate')
63 | ->willReturn($mx);
64 | $mValidator = new \ReflectionProperty($emailValidator, 'mxValidator');
65 | $mValidator->setAccessible(true);
66 | $mValidator->setValue($emailValidator, $mxValidator);
67 |
68 | $bannedValidator = $this->createMock(BannedListValidator::class);
69 | $bannedValidator->method('validate')
70 | ->willReturn($banned);
71 | $bnValidator = new \ReflectionProperty($emailValidator, 'bannedListValidator');
72 | $bnValidator->setAccessible(true);
73 | $bnValidator->setValue($emailValidator, $bannedValidator);
74 |
75 | $disposableValidator = $this->createMock(DisposableEmailValidator::class);
76 | $disposableValidator->method('validate')
77 | ->willReturn($disposable);
78 | $dValidator = new \ReflectionProperty($emailValidator, 'disposableEmailValidator');
79 | $dValidator->setAccessible(true);
80 | $dValidator->setValue($emailValidator, $disposableValidator);
81 |
82 | $freeValidator = $this->createMock(FreeEmailValidator::class);
83 | $freeValidator->method('validate')
84 | ->willReturn($free);
85 | $fValidator = new \ReflectionProperty($emailValidator, 'freeEmailValidator');
86 | $fValidator->setAccessible(true);
87 | $fValidator->setValue($emailValidator, $freeValidator);
88 |
89 | $reason = new \ReflectionProperty($emailValidator, 'reason');
90 | $reason->setAccessible(true);
91 | $emailValidator->validate('user@example.com');
92 |
93 | self::assertEquals($errCode, $reason->getValue($emailValidator));
94 | }
95 |
96 | public function errorReasonDataProvider(): array
97 | {
98 | return [
99 | [EmailValidator::FAIL_BASIC, 'Invalid format'],
100 | [EmailValidator::FAIL_RFC5322, 'Does not comply with RFC 5322'],
101 | [EmailValidator::FAIL_MX_RECORD, 'Domain does not accept email'],
102 | [EmailValidator::FAIL_BANNED_DOMAIN, 'Domain is banned'],
103 | [EmailValidator::FAIL_DISPOSABLE_DOMAIN, 'Domain is used by disposable email providers'],
104 | [EmailValidator::FAIL_FREE_PROVIDER, 'Domain is used by free email providers'],
105 | [EmailValidator::NO_ERROR, ''],
106 | ];
107 | }
108 |
109 | /**
110 | * @dataProvider errorReasonDataProvider
111 | * @param int $errorCode
112 | * @param string $errorMsg
113 | */
114 | public function testGetErrorReason(int $errorCode, string $errorMsg): void
115 | {
116 | $emailValidator = new EmailValidator();
117 |
118 | $reason = new \ReflectionProperty(EmailValidator::class, 'reason');
119 | $reason->setAccessible(true);
120 | $reason->setValue($emailValidator, $errorCode);
121 |
122 | self::assertEquals($errorMsg, $emailValidator->getErrorReason());
123 | }
124 |
125 | public function testGetErrorCode(): void
126 | {
127 | self::assertEquals(EmailValidator::NO_ERROR, (new EmailValidator())->getErrorCode());
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/tests/EmailValidator/PolicyTest.php:
--------------------------------------------------------------------------------
1 | validateMxRecord());
15 | self::assertFalse($policy->checkBannedListedEmail());
16 | self::assertFalse($policy->checkDisposableEmail());
17 | self::assertFalse($policy->checkFreeEmail());
18 | self::assertFalse($policy->checkDisposableLocalListOnly());
19 | self::assertFalse($policy->checkFreeLocalListOnly());
20 | self::assertIsArray($policy->getBannedList());
21 | self::assertEmpty($policy->getBannedList());
22 | self::assertIsArray($policy->getDisposableList());
23 | self::assertEmpty($policy->getDisposableList());
24 | self::assertIsArray($policy->getFreeList());
25 | self::assertEmpty($policy->getFreeList());
26 | }
27 |
28 | public function booleanConfigDataProvider(): array
29 | {
30 | return [
31 | [true, true],
32 | [false, false],
33 | [1, true],
34 | [0, false],
35 | ['true', true],
36 | ['test', true],
37 | ];
38 | }
39 |
40 | /**
41 | * @dataProvider booleanConfigDataProvider
42 | * @param $config
43 | * @param bool $setting
44 | */
45 | public function testNonDefaultBooleanSetting($config, bool $setting): void
46 | {
47 | $policy = new Policy(['checkMxRecords' => $config]);
48 | self::assertEquals($setting, $policy->validateMxRecord());
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/BannedListValidatorTest.php:
--------------------------------------------------------------------------------
1 | $enabled,
40 | 'bannedList' => $bannedList
41 | ];
42 | $validator = new BannedListValidator(new Policy($policy));
43 | self::assertEquals($valid, $validator->validate(new EmailAddress($email)));
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/BasicValidatorTest.php:
--------------------------------------------------------------------------------
1 | validate(new EmailAddress($email));
29 | self::assertEquals($valid, $isValid);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/CustomValidatorTest.php:
--------------------------------------------------------------------------------
1 | validate(new EmailAddress('user@example.com')));
16 | self::assertFalse($validator->validate(new EmailAddress('user@gmail.com')));
17 | }
18 |
19 | public function testCustomValidatorWithPolicy(): void
20 | {
21 | $policy = new Policy(['customSetting' => true]);
22 | $validator = new ExampleDotComValidator($policy);
23 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
24 | self::assertFalse($validator->validate(new EmailAddress('user@gmail.com')));
25 | }
26 |
27 | public function testCustomValidatorWithEmailValidator(): void
28 | {
29 | $emailValidator = new EmailValidator();
30 | $emailValidator->registerValidator(new ExampleDotComValidator(new Policy()));
31 |
32 | self::assertTrue($emailValidator->validate('user@example.com'));
33 | self::assertFalse($emailValidator->validate('user@gmail.com'));
34 | self::assertEquals(EmailValidator::FAIL_CUSTOM, $emailValidator->getErrorCode());
35 | self::assertEquals('Failed custom validation', $emailValidator->getErrorReason());
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/DisposableEmailValidatorTest.php:
--------------------------------------------------------------------------------
1 | false
16 | ];
17 | $validator = new DisposableEmailValidator(new Policy($policy));
18 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
19 | }
20 |
21 | public function testValidateDefault(): void
22 | {
23 | $validator = new DisposableEmailValidator(new Policy());
24 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
25 | }
26 |
27 | public function testValidateClientProvidedDomain(): void
28 | {
29 | $policy = [
30 | 'checkDisposableEmail' => true,
31 | 'disposableList' => [
32 | 'example.com'
33 | ],
34 | ];
35 | $validator = new DisposableEmailValidator(new Policy($policy));
36 | self::assertFalse($validator->validate(new EmailAddress('user@example.com')));
37 | }
38 |
39 | public function testValidateInvalidEmail(): void
40 | {
41 | $policy = [
42 | 'checkDisposableEmail' => true,
43 | 'disposableList' => [
44 | 'example.com'
45 | ],
46 | ];
47 | $validator = new DisposableEmailValidator(new Policy($policy));
48 | self::assertTrue($validator->validate(new EmailAddress('invalid-email')));
49 | }
50 |
51 | public function testValidateEmptyDomain(): void
52 | {
53 | $policy = [
54 | 'checkDisposableEmail' => true,
55 | 'disposableList' => [
56 | 'example.com'
57 | ],
58 | ];
59 | $validator = new DisposableEmailValidator(new Policy($policy));
60 | self::assertTrue($validator->validate(new EmailAddress('user@')));
61 | }
62 |
63 | public function testValidateWithEmptyList(): void
64 | {
65 | $policy = [
66 | 'checkDisposableEmail' => true,
67 | 'disposableList' => [],
68 | ];
69 | $validator = new DisposableEmailValidator(new Policy($policy));
70 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/Domain/DomainLiteralValidatorTest.php:
--------------------------------------------------------------------------------
1 | validator = new DomainLiteralValidator();
17 | }
18 |
19 | /**
20 | * @dataProvider validDomainLiteralsProvider
21 | */
22 | public function testValidDomainLiterals(string $domain): void
23 | {
24 | $this->assertTrue($this->validator->validate($domain));
25 | }
26 |
27 | /**
28 | * @dataProvider invalidDomainLiteralsProvider
29 | */
30 | public function testInvalidDomainLiterals(string $domain): void
31 | {
32 | $this->assertFalse($this->validator->validate($domain));
33 | }
34 |
35 | public function validDomainLiteralsProvider(): array
36 | {
37 | return [
38 | 'simple IPv4' => ['[192.168.1.1]'],
39 | 'IPv4 with leading zeros' => ['[192.168.001.001]'],
40 | 'IPv4 max values' => ['[255.255.255.255]'],
41 | 'IPv4 min values' => ['[0.0.0.0]'],
42 | 'simple IPv6' => ['[IPv6:2001:db8::1]'],
43 | 'full IPv6' => ['[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]'],
44 | 'IPv6 uppercase' => ['[IPv6:2001:DB8::1]'],
45 | 'IPv6 mixed case' => ['[IPv6:2001:dB8::1]'],
46 | 'IPv6 with zeros' => ['[IPv6:0:0:0:0:0:0:0:1]'],
47 | 'IPv6 localhost' => ['[IPv6:0:0:0:0:0:0:0:1]'],
48 | 'IPv6 all max' => ['[IPv6:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF]'],
49 | ];
50 | }
51 |
52 | public function invalidDomainLiteralsProvider(): array
53 | {
54 | return [
55 | 'empty string' => [''],
56 | 'missing brackets' => ['192.168.1.1'],
57 | 'missing start bracket' => ['192.168.1.1]'],
58 | 'missing end bracket' => ['[192.168.1.1'],
59 | 'empty brackets' => ['[]'],
60 | 'IPv4 empty octet' => ['[192.168..1]'],
61 | 'IPv4 too few octets' => ['[192.168.1]'],
62 | 'IPv4 too many octets' => ['[192.168.1.1.1]'],
63 | 'IPv4 invalid octet' => ['[192.168.1.256]'],
64 | 'IPv4 negative octet' => ['[192.168.1.-1]'],
65 | 'IPv4 non-numeric' => ['[192.168.1.abc]'],
66 | 'IPv6 missing prefix' => ['[2001:db8::1]'],
67 | 'IPv6 wrong prefix' => ['[IPV6:2001:db8::1]'],
68 | 'IPv6 too few groups' => ['[IPv6:2001:db8]'],
69 | 'IPv6 too many groups' => ['[IPv6:2001:db8:1:2:3:4:5:6:7]'],
70 | 'IPv6 invalid chars' => ['[IPv6:2001:db8::g]'],
71 | 'IPv6 group too long' => ['[IPv6:20011:db8::1]'],
72 | 'mixed content' => ['[text and numbers]'],
73 | 'with spaces' => ['[192.168.1.1 ]'],
74 | 'with newline' => ["[192.168.1.1\n]"],
75 | 'with tab' => ["[192.168.1.1\t]"],
76 | ];
77 | }
78 | }
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/Domain/DomainNameValidatorTest.php:
--------------------------------------------------------------------------------
1 | validator = new DomainNameValidator();
17 | }
18 |
19 | /**
20 | * @dataProvider validDomainsProvider
21 | */
22 | public function testValidDomains(string $domain): void
23 | {
24 | $this->assertTrue($this->validator->validate($domain));
25 | }
26 |
27 | /**
28 | * @dataProvider invalidDomainsProvider
29 | */
30 | public function testInvalidDomains(string $domain): void
31 | {
32 | $this->assertFalse($this->validator->validate($domain));
33 | }
34 |
35 | public function validDomainsProvider(): array
36 | {
37 | return [
38 | 'simple domain' => ['example.com'],
39 | 'subdomain' => ['sub.example.com'],
40 | 'multiple subdomains' => ['a.b.c.example.com'],
41 | 'numeric domain' => ['123.example.com'],
42 | 'alphanumeric' => ['test123.example.com'],
43 | 'with hyphen' => ['my-domain.example.com'],
44 | 'hyphen in multiple parts' => ['my-domain.my-example.com'],
45 | 'single character parts' => ['a.b.c.d'],
46 | 'max length label' => [str_repeat('a', 63) . '.com'],
47 | 'idn domain' => ['xn--80akhbyknj4f.com'], // IDN example
48 | 'complex mix' => ['sub-123.example-domain.com'],
49 | ];
50 | }
51 |
52 | public function invalidDomainsProvider(): array
53 | {
54 | return [
55 | 'empty string' => [''],
56 | 'single label' => ['localhost'],
57 | 'starts with dot' => ['.example.com'],
58 | 'ends with dot' => ['example.com.'],
59 | 'consecutive dots' => ['example..com'],
60 | 'starts with hyphen' => ['-example.com'],
61 | 'ends with hyphen' => ['example-.com'],
62 | 'consecutive hyphens' => ['ex--ample.com'],
63 | 'too long label' => [str_repeat('a', 64) . '.com'],
64 | 'too long domain' => [str_repeat('a.', 127) . 'com'], // Over 255 chars
65 | 'invalid chars' => ['example_domain.com'],
66 | 'with space' => ['example domain.com'],
67 | 'with tab' => ["example\tdomain.com"],
68 | 'with newline' => ["example\ndomain.com"],
69 | 'with @' => ['example@domain.com'],
70 | 'with brackets' => ['example[1].com'],
71 | 'with parentheses' => ['example(1).com'],
72 | 'with special chars' => ['example!.com'],
73 | ];
74 | }
75 | }
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/ExampleDotComValidator.php:
--------------------------------------------------------------------------------
1 | getDomain() === 'example.com';
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/FreeEmailValidatorTest.php:
--------------------------------------------------------------------------------
1 | false
15 | ];
16 | $validator = new FreeEmailValidator(new Policy($policy));
17 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
18 | }
19 |
20 | public function testValidateDefault(): void
21 | {
22 | $validator = new FreeEmailValidator(new Policy());
23 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
24 | }
25 |
26 | public function testValidateClientProvidedDomain(): void
27 | {
28 | $policy = [
29 | 'checkFreeEmail' => true,
30 | 'freeList' => [
31 | 'example.com'
32 | ],
33 | ];
34 | $validator = new FreeEmailValidator(new Policy($policy));
35 | self::assertFalse($validator->validate(new EmailAddress('user@example.com')));
36 | }
37 |
38 | public function testValidateInvalidEmail(): void
39 | {
40 | $policy = [
41 | 'checkFreeEmail' => true,
42 | 'freeList' => [
43 | 'example.com'
44 | ],
45 | ];
46 | $validator = new FreeEmailValidator(new Policy($policy));
47 | self::assertTrue($validator->validate(new EmailAddress('invalid-email')));
48 | }
49 |
50 | public function testValidateEmptyDomain(): void
51 | {
52 | $policy = [
53 | 'checkFreeEmail' => true,
54 | 'freeList' => [
55 | 'example.com'
56 | ],
57 | ];
58 | $validator = new FreeEmailValidator(new Policy($policy));
59 | self::assertTrue($validator->validate(new EmailAddress('user@')));
60 | }
61 |
62 | public function testValidateWithEmptyList(): void
63 | {
64 | $policy = [
65 | 'checkFreeEmail' => true,
66 | 'freeList' => [],
67 | ];
68 | $validator = new FreeEmailValidator(new Policy($policy));
69 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/GmailValidatorTest.php:
--------------------------------------------------------------------------------
1 | validator = new GmailValidator(new Policy());
22 | }
23 |
24 | /**
25 | * @dataProvider validateProvider
26 | * @since 2.0.0
27 | */
28 | public function testValidate(string $email, bool $expected): void
29 | {
30 | $this->assertEquals($expected, $this->validator->validate(new EmailAddress($email)));
31 | }
32 |
33 | /**
34 | * @since 2.0.0
35 | */
36 | public function validateProvider(): array
37 | {
38 | return [
39 | ['test@gmail.com', true],
40 | ['test@googlemail.com', true],
41 | ['test@example.com', false],
42 | ['test+alias@gmail.com', true],
43 | ['test.alias@gmail.com', true],
44 | ];
45 | }
46 |
47 | /**
48 | * @dataProvider isGmailWithPlusCharProvider
49 | * @since 2.0.0
50 | */
51 | public function testIsGmailWithPlusChar(string $email, bool $expected): void
52 | {
53 | $this->assertEquals($expected, $this->validator->isGmailWithPlusChar(new EmailAddress($email)));
54 | }
55 |
56 | /**
57 | * @since 2.0.0
58 | */
59 | public function isGmailWithPlusCharProvider(): array
60 | {
61 | return [
62 | ['test@gmail.com', false],
63 | ['test+alias@gmail.com', true],
64 | ['test+alias@googlemail.com', true],
65 | ['test@example.com', false],
66 | ['test+alias@example.com', false],
67 | ];
68 | }
69 |
70 | /**
71 | * @dataProvider getGmailAddressWithoutPlusProvider
72 | * @since 2.0.0
73 | */
74 | public function testGetGmailAddressWithoutPlus(string $email, string $expected): void
75 | {
76 | $this->assertEquals($expected, $this->validator->getGmailAddressWithoutPlus(new EmailAddress($email)));
77 | }
78 |
79 | /**
80 | * @since 2.0.0
81 | */
82 | public function getGmailAddressWithoutPlusProvider(): array
83 | {
84 | return [
85 | ['test@gmail.com', 'test@gmail.com'],
86 | ['test+alias@gmail.com', 'test@gmail.com'],
87 | ['test+alias@googlemail.com', 'test@googlemail.com'],
88 | ['test@example.com', 'test@example.com'],
89 | ['test+alias@example.com', 'test+alias@example.com'],
90 | ];
91 | }
92 |
93 | /**
94 | * @dataProvider getSanitizedGmailAddressProvider
95 | * @since 2.0.0
96 | */
97 | public function testGetSanitizedGmailAddress(string $email, string $expected): void
98 | {
99 | $this->assertEquals($expected, $this->validator->getSanitizedGmailAddress(new EmailAddress($email)));
100 | }
101 |
102 | /**
103 | * @since 2.0.0
104 | */
105 | public function getSanitizedGmailAddressProvider(): array
106 | {
107 | return [
108 | ['test@gmail.com', 'test@gmail.com'],
109 | ['test+alias@gmail.com', 'test@gmail.com'],
110 | ['test.alias@gmail.com', 'testalias@gmail.com'],
111 | ['test+alias@googlemail.com', 'test@googlemail.com'],
112 | ['test.alias+alias@gmail.com', 'testalias@gmail.com'],
113 | ['test@example.com', 'test@example.com'],
114 | ['test+alias@example.com', 'test+alias@example.com'],
115 | ];
116 | }
117 | }
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/LocalPart/AtomValidatorTest.php:
--------------------------------------------------------------------------------
1 | validator = new AtomValidator();
17 | }
18 |
19 | /**
20 | * @dataProvider validLocalPartsProvider
21 | */
22 | public function testValidLocalParts(string $localPart): void
23 | {
24 | $this->assertTrue($this->validator->validate($localPart));
25 | }
26 |
27 | /**
28 | * @dataProvider invalidLocalPartsProvider
29 | */
30 | public function testInvalidLocalParts(string $localPart): void
31 | {
32 | $this->assertFalse($this->validator->validate($localPart));
33 | }
34 |
35 | public function validLocalPartsProvider(): array
36 | {
37 | return [
38 | 'simple' => ['simple'],
39 | 'alphanumeric' => ['user123'],
40 | 'with allowed special chars' => ['user.name'],
41 | 'multiple dots' => ['first.middle.last'],
42 | 'all special chars' => ['!#$%&\'*+-/=?^_`{|}~'],
43 | 'mixed case' => ['First.Last'],
44 | 'numbers and special chars' => ['user+123'],
45 | 'underscore' => ['user_name'],
46 | 'complex mix' => ['first.last+tag'],
47 | ];
48 | }
49 |
50 | public function invalidLocalPartsProvider(): array
51 | {
52 | return [
53 | 'empty string' => [''],
54 | 'single dot' => ['.'],
55 | 'starts with dot' => ['.user'],
56 | 'ends with dot' => ['user.'],
57 | 'consecutive dots' => ['user..name'],
58 | 'space' => ['user name'],
59 | 'tab' => ["user\tname"],
60 | 'newline' => ["user\nname"],
61 | 'invalid chars' => ['user@name'],
62 | 'brackets' => ['user[name]'],
63 | 'quotes' => ['user"name'],
64 | 'backslash' => ['user\\name'],
65 | 'comma' => ['user,name'],
66 | 'semicolon' => ['user;name'],
67 | 'greater than' => ['user>name'],
68 | 'less than' => ['uservalidator = new QuotedStringValidator();
17 | }
18 |
19 | /**
20 | * @dataProvider validQuotedStringsProvider
21 | */
22 | public function testValidQuotedStrings(string $quotedString): void
23 | {
24 | $this->assertTrue($this->validator->validate($quotedString));
25 | }
26 |
27 | /**
28 | * @dataProvider invalidQuotedStringsProvider
29 | */
30 | public function testInvalidQuotedStrings(string $quotedString): void
31 | {
32 | $this->assertFalse($this->validator->validate($quotedString));
33 | }
34 |
35 | public function validQuotedStringsProvider(): array
36 | {
37 | return [
38 | 'empty quoted string' => ['""'],
39 | 'simple text' => ['"simple"'],
40 | 'with spaces' => ['"user name"'],
41 | 'with dots' => ['"user.name"'],
42 | 'with special chars' => ['"!#$%&\'*+-/=?^_`{|}~"'],
43 | 'escaped quote' => ['"user\"name"'],
44 | 'escaped backslash' => ['"user\\\\name"'],
45 | 'multiple escaped chars' => [<<<'EOD'
46 | "user\"\\name"
47 | EOD],
48 | 'with at symbol' => ['"user@name"'],
49 | 'with brackets' => ['"user[name]"'],
50 | 'with parentheses' => ['"user(name)"'],
51 | 'with angle brackets' => ['"user"'],
52 | 'with curly braces' => ['"user{name}"'],
53 | 'with semicolon' => ['"user;name"'],
54 | 'with comma' => ['"user,name"'],
55 | 'with tab' => ["\"user\tname\""],
56 | 'complex mix' => ['"user.\"name\"@[domain]"'],
57 | ];
58 | }
59 |
60 | public function invalidQuotedStringsProvider(): array
61 | {
62 | return [
63 | 'empty string' => [''],
64 | 'unquoted string' => ['simple'],
65 | 'missing start quote' => ['simple"'],
66 | 'missing end quote' => ['"simple'],
67 | 'unescaped quote' => ['"user"name"'],
68 | 'lone backslash at end' => ['"user\\"'],
69 | 'invalid escape sequence' => ['"user\name"'],
70 | 'non-printable char' => ["\"\x01\""],
71 | 'high ascii char' => ["\"\x80\""],
72 | 'newline' => ["\"user\nname\""],
73 | 'carriage return' => ["\"user\rname\""],
74 | 'null byte' => ["\"user\0name\""],
75 | ];
76 | }
77 | }
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/MxValidatorTest.php:
--------------------------------------------------------------------------------
1 | false
16 | ];
17 | $validator = new MxValidator(new Policy($policy));
18 | self::assertTrue($validator->validate(new EmailAddress('user@example.com')));
19 | }
20 |
21 | public function testValidateDns(): void
22 | {
23 | $policy = [
24 | 'checkMxRecords' => true
25 | ];
26 | $validator = new MxValidator(new Policy($policy));
27 | self::assertTrue($validator->validate(new EmailAddress('stymiee@gmail.com')));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/ProviderValidatorTest.php:
--------------------------------------------------------------------------------
1 | getList(true, $domains));
18 | }
19 |
20 | public function testGetExternalListJson(): void
21 | {
22 | $provider = new FreeEmailValidator(new Policy());
23 | $reflectionMethod = new \ReflectionMethod($provider, 'getExternalList');
24 | $reflectionMethod->setAccessible(true);
25 |
26 | $domains = ['example.com', 'test.com'];
27 | self::assertEquals($domains, $reflectionMethod->invoke($provider, json_encode($domains), 'json'));
28 | }
29 |
30 | public function testGetExternalListTxt(): void
31 | {
32 | $provider = new FreeEmailValidator(new Policy());
33 | $reflectionMethod = new \ReflectionMethod($provider, 'getExternalList');
34 | $reflectionMethod->setAccessible(true);
35 |
36 | $domains = ['example.com', 'test.com'];
37 | self::assertEquals($domains, $reflectionMethod->invoke($provider, implode("\r\n", $domains), 'txt'));
38 | }
39 |
40 | public function testGetExternalListInvalidJson(): void
41 | {
42 | $provider = new FreeEmailValidator(new Policy());
43 | $reflectionMethod = new \ReflectionMethod($provider, 'getExternalList');
44 | $reflectionMethod->setAccessible(true);
45 |
46 | self::assertEquals([], $reflectionMethod->invoke($provider, 'invalid json', 'json'));
47 | }
48 |
49 | public function testGetExternalListMixedTypes(): void
50 | {
51 | $provider = new FreeEmailValidator(new Policy());
52 | $reflectionMethod = new \ReflectionMethod($provider, 'getExternalList');
53 | $reflectionMethod->setAccessible(true);
54 |
55 | $mixedData = ['example.com', 123, true, null, 'test.com'];
56 | $expected = ['example.com', 'test.com'];
57 | self::assertEquals($expected, $reflectionMethod->invoke($provider, json_encode($mixedData), 'json'));
58 | }
59 |
60 | public function testGetExternalListEmptyContent(): void
61 | {
62 | $provider = new FreeEmailValidator(new Policy());
63 | $reflectionMethod = new \ReflectionMethod($provider, 'getExternalList');
64 | $reflectionMethod->setAccessible(true);
65 |
66 | self::assertEquals([], $reflectionMethod->invoke($provider, '', 'txt'));
67 | self::assertEquals([], $reflectionMethod->invoke($provider, '', 'json'));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/EmailValidator/Validator/Rfc5322ValidatorTest.php:
--------------------------------------------------------------------------------
1 | validator = new Rfc5322Validator();
18 | }
19 |
20 | /**
21 | * @dataProvider validEmailsProvider
22 | */
23 | public function testValidEmails(string $email): void
24 | {
25 | $emailObj = new EmailAddress($email);
26 | $this->assertTrue($this->validator->validate($emailObj));
27 | }
28 |
29 | /**
30 | * @dataProvider invalidEmailsProvider
31 | */
32 | public function testInvalidEmails(string $email): void
33 | {
34 | $emailObj = new EmailAddress($email);
35 | $this->assertFalse($this->validator->validate($emailObj));
36 | }
37 |
38 | public function validEmailsProvider(): array
39 | {
40 | return [
41 | // Regular email addresses
42 | 'simple' => ['user@example.com'],
43 | 'with numbers' => ['user123@example.com'],
44 | 'with special chars' => ['user+tag@example.com'],
45 | 'with dots' => ['first.last@example.com'],
46 | 'subdomain' => ['user@sub.example.com'],
47 |
48 | // Local part variations
49 | 'quoted simple' => ['"user"@example.com'],
50 | 'quoted with spaces' => ['"John Doe"@example.com'],
51 | 'quoted with special chars' => ['"user@name"@example.com'],
52 | 'quoted with escaped chars' => ['"user\"name"@example.com'],
53 | 'complex local part' => ['first.middle.last+tag@example.com'],
54 |
55 | // Domain variations
56 | 'IPv4 domain' => ['user@[192.168.1.1]'],
57 | 'IPv4 with zeros' => ['user@[192.168.001.001]'],
58 | 'IPv6 domain' => ['user@[IPv6:2001:db8::1]'],
59 | 'multiple subdomains' => ['user@a.b.c.example.com'],
60 | 'domain with hyphen' => ['user@my-domain.example.com'],
61 |
62 | // Complex combinations
63 | 'complex quoted with IPv4' => ['"user.name"@[192.168.1.1]'],
64 | 'complex quoted with IPv6' => ['"user.name"@[IPv6:2001:db8::1]'],
65 | 'all special chars local' => ['!#$%&\'*+-/=?^_`{|}~@example.com'],
66 | ];
67 | }
68 |
69 | public function invalidEmailsProvider(): array
70 | {
71 | return [
72 | // Basic validation
73 | 'empty string' => [''],
74 | 'no at symbol' => ['userexample.com'],
75 | 'multiple at symbols' => ['user@domain@example.com'],
76 | 'empty local part' => ['@example.com'],
77 | 'empty domain' => ['user@'],
78 |
79 | // Local part validation
80 | 'local part too long' => [str_repeat('a', 65) . '@example.com'],
81 | 'unescaped quote' => ['"user"name"@example.com'],
82 | 'invalid chars in local' => ['user name@example.com'],
83 | 'dot at start' => ['.user@example.com'],
84 | 'dot at end' => ['user.@example.com'],
85 | 'consecutive dots' => ['user..name@example.com'],
86 |
87 | // Domain validation
88 | 'single label domain' => ['user@localhost'],
89 | 'invalid domain chars' => ['user@example_domain.com'],
90 | 'domain starts with dot' => ['user@.example.com'],
91 | 'domain ends with dot' => ['user@example.com.'],
92 | 'consecutive dots in domain' => ['user@example..com'],
93 | 'domain starts with hyphen' => ['user@-example.com'],
94 | 'domain ends with hyphen' => ['user@example-.com'],
95 | 'domain too long' => ['user@' . str_repeat('a.', 127) . 'com'],
96 |
97 | // Domain literal validation
98 | 'invalid IPv4' => ['user@[192.168.1.256]'],
99 | 'incomplete IPv4' => ['user@[192.168.1]'],
100 | 'invalid IPv6 prefix' => ['user@[IPV6:2001:db8::1]'],
101 | 'invalid IPv6 groups' => ['user@[IPv6:2001:db8]'],
102 | 'invalid brackets' => ['user@192.168.1.1]'],
103 |
104 | // Special cases
105 | 'with line break' => ["user@example\n.com"],
106 | 'with carriage return' => ["user@example\r.com"],
107 | 'with tab' => ["user@example\t.com"],
108 | 'with null byte' => ["user@example\0.com"],
109 | ];
110 | }
111 | }
112 |
--------------------------------------------------------------------------------