├── .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 | [![Latest Stable Version](https://poser.pugx.org/stymiee/email-validator/v/stable.svg)](https://packagist.org/packages/stymiee/email-validator) 2 | [![Total Downloads](https://poser.pugx.org/stymiee/email-validator/downloads)](https://packagist.org/packages/stymiee/email-validator) 3 | ![Build](https://github.com/stymiee/email-validator/workflows/Build/badge.svg) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/stymiee/email-validator/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/stymiee/email-validator/?branch=master) 5 | [![License](https://poser.pugx.org/stymiee/email-validator/license)](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 | --------------------------------------------------------------------------------