From a66dd1372df8da667a835502af2a54a0f1948613 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 18 Oct 2025 22:33:32 +0100 Subject: [PATCH 01/21] Init --- .github/workflows/codeql-analysis.yml | 21 + .github/workflows/linter.yml | 21 + .github/workflows/test.yml | 29 + .gitignore | 323 + CODE_OF_CONDUCT.md | 75 + CONTRIBUTING.md | 181 + README.md | 341 + composer.json | 51 + data/disposable-domains-manual.php | 14 + data/disposable-domains.php | 72422 ++++++++++++++++++ data/free-domains-manual.php | 14 + data/free-domains.php | 14 + data/sources.php | 61 + import.php | 846 + phpunit.xml | 17 + pint.json | 10 + src/Emails/Email.php | 280 + src/Emails/Validator/Email.php | 72 + src/Emails/Validator/EmailCorporate.php | 73 + src/Emails/Validator/EmailDomain.php | 73 + src/Emails/Validator/EmailLocal.php | 73 + src/Emails/Validator/EmailNotDisposable.php | 73 + test-import.php | 133 + tests/EmailTest.php | 384 + tests/Validator/EmailCorporateTest.php | 122 + tests/Validator/EmailDomainTest.php | 82 + tests/Validator/EmailLocalTest.php | 79 + tests/Validator/EmailNotDisposableTest.php | 97 + tests/Validator/EmailTest.php | 93 + 29 files changed, 76074 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/linter.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 data/disposable-domains-manual.php create mode 100644 data/disposable-domains.php create mode 100644 data/free-domains-manual.php create mode 100644 data/free-domains.php create mode 100644 data/sources.php create mode 100644 import.php create mode 100644 phpunit.xml create mode 100644 pint.json create mode 100644 src/Emails/Email.php create mode 100644 src/Emails/Validator/Email.php create mode 100644 src/Emails/Validator/EmailCorporate.php create mode 100644 src/Emails/Validator/EmailDomain.php create mode 100644 src/Emails/Validator/EmailLocal.php create mode 100644 src/Emails/Validator/EmailNotDisposable.php create mode 100644 test-import.php create mode 100644 tests/EmailTest.php create mode 100644 tests/Validator/EmailCorporateTest.php create mode 100644 tests/Validator/EmailDomainTest.php create mode 100644 tests/Validator/EmailLocalTest.php create mode 100644 tests/Validator/EmailNotDisposableTest.php create mode 100644 tests/Validator/EmailTest.php diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..6436663 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,21 @@ +name: "CodeQL" + +on: [pull_request] +jobs: + lint: + name: CodeQL + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - run: git checkout HEAD^2 + + - name: Run CodeQL + run: | + docker run --rm -v $PWD:/app composer:2.6 sh -c \ + "composer install --profile --ignore-platform-reqs && composer check" + diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..e27f301 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,21 @@ +name: "Linter" + +on: [pull_request] +jobs: + lint: + name: Linter + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - run: git checkout HEAD^2 + + - name: Run Linter + run: | + docker run --rm -v $PWD:/app composer sh -c \ + "composer install --profile --ignore-platform-reqs && composer lint" + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9dc5183 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: "Tests" + +on: [pull_request] +jobs: + lint: + name: Tests ${{ matrix.php-versions }} + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ['8.1', '8.2', '8.3', 'nightly'] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup PHP ${{ matrix.php-versions }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Compose install + run: composer install --ignore-platform-reqs + + - name: Run tests + run: composer test + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3326a3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,323 @@ +# Composer +/vendor/ +composer.phar +composer.lock + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Runtime +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ + +# Build artifacts +build/ +dist/ + +# PHPUnit +.phpunit.result.cache + +# PHPStan +.phpstan.neon + +# PHP CS Fixer +.php_cs.cache + +# Laravel Pint +.php-cs-fixer.cache + +# PHP_CodeSniffer +.phpcs.xml + +# Psalm +psalm.xml +psalm-baseline.xml + +# PHPUnit coverage +clover.xml +coverage.xml +coverage/ + +# PHPUnit cache +.phpunit.result.cache + +# PHPStan cache +.phpstan.cache + +# PHP CS Fixer cache +.php_cs.cache + +# Laravel Pint cache +.php-cs-fixer.cache + +# PHP_CodeSniffer cache +.phpcs.cache + +# Psalm cache +.psalm/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Log files +*.log + +# Runtime files +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage/ + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ + +# Build artifacts +build/ +dist/ + +# PHPUnit +.phpunit.result.cache + +# PHPStan +.phpstan.neon + +# PHP CS Fixer +.php_cs.cache + +# Laravel Pint +.php-cs-fixer.cache + +# PHP_CodeSniffer +.phpcs.xml + +# Psalm +psalm.xml +psalm-baseline.xml + +# PHPUnit coverage +clover.xml +coverage.xml +coverage/ + +# PHPUnit cache +.phpunit.result.cache + +# PHPStan cache +.phpstan.cache + +# PHP CS Fixer cache +.php_cs.cache + +# Laravel Pint cache +.php-cs-fixer.cache + +# PHP_CodeSniffer cache +.phpcs.cache + +# Psalm cache +.psalm/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Log files +*.log + +# Runtime files +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage/ + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ + +# Build artifacts +build/ +dist/ + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8fa1d37 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,75 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing the project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d84c753 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,181 @@ +# Contributing to Utopia Emails + +Thank you for your interest in contributing to Utopia Emails! This document provides guidelines and information for contributors. + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/your-username/emails.git` +3. Create a new branch: `git checkout -b feature/your-feature-name` +4. Make your changes +5. Run tests: `composer test` +6. Run linting: `composer lint` +7. Run static analysis: `composer check` +8. Commit your changes: `git commit -m "Add your feature"` +9. Push to your fork: `git push origin feature/your-feature-name` +10. Create a Pull Request + +## Development Setup + +### Prerequisites + +- PHP 8.0 or later +- Composer + +### Installation + +```bash +git clone https://github.com/utopia-php/emails.git +cd emails +composer install +``` + +### Running Tests + +```bash +composer test +``` + +### Code Style + +We use Laravel Pint for code formatting. Run the following commands: + +```bash +# Check code style +composer lint + +# Fix code style issues +composer format +``` + +### Static Analysis + +We use PHPStan for static analysis: + +```bash +composer check +``` + +## Code Standards + +### PHP Standards + +- Follow PSR-12 coding standards +- Use type hints for all parameters and return types +- Write comprehensive PHPDoc comments +- Use meaningful variable and method names +- Keep methods small and focused +- Write unit tests for all new functionality + +### Testing Standards + +- Write unit tests for all new features +- Aim for high test coverage +- Use descriptive test method names +- Test both positive and negative cases +- Test edge cases and error conditions + +### Documentation Standards + +- Update README.md for new features +- Add PHPDoc comments for all public methods +- Include usage examples in documentation +- Keep documentation up to date with code changes + +## Pull Request Guidelines + +### Before Submitting + +1. Ensure all tests pass +2. Run code style checks and fix any issues +3. Run static analysis and fix any issues +4. Update documentation if needed +5. Add tests for new functionality + +### Pull Request Template + +When creating a pull request, please include: + +- A clear description of the changes +- Reference to any related issues +- Screenshots or examples if applicable +- Testing instructions if needed + +### Review Process + +- All pull requests require review +- Address feedback promptly +- Keep pull requests focused and small +- Rebase on main branch if needed + +## Issue Guidelines + +### Bug Reports + +When reporting bugs, please include: + +- PHP version +- Library version +- Steps to reproduce +- Expected behavior +- Actual behavior +- Error messages or logs + +### Feature Requests + +When requesting features, please include: + +- Use case description +- Proposed solution +- Alternative solutions considered +- Additional context + +## Development Workflow + +### Branch Naming + +- `feature/description` - New features +- `bugfix/description` - Bug fixes +- `hotfix/description` - Critical fixes +- `docs/description` - Documentation updates +- `refactor/description` - Code refactoring + +### Commit Messages + +Use clear, descriptive commit messages: + +- Use imperative mood ("Add feature" not "Added feature") +- Keep the first line under 50 characters +- Add more details in the body if needed +- Reference issues when applicable + +### Release Process + +1. Update version in composer.json +2. Update CHANGELOG.md +3. Create a release tag +4. Publish to Packagist + +## Community Guidelines + +- Be respectful and inclusive +- Help others learn and grow +- Provide constructive feedback +- Follow the Code of Conduct +- Be patient with newcomers + +## Getting Help + +- Check existing issues and discussions +- Ask questions in GitHub Discussions +- Join our Discord community +- Contact maintainers directly + +## License + +By contributing to Utopia Emails, you agree that your contributions will be licensed under the MIT License. + +## Thank You + +Thank you for contributing to Utopia Emails! Your contributions help make this project better for everyone. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebd0b85 --- /dev/null +++ b/README.md @@ -0,0 +1,341 @@ +# Utopia Emails + +[![Build Status](https://travis-ci.org/utopia-php/emails.svg?branch=master)](https://travis-ci.com/utopia-php/emails) +![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/emails.svg) +[![Discord](https://img.shields.io/discord/564160730845151244)](https://appwrite.io/discord) + +Utopia Emails library is a simple and lite library for parsing and validating email addresses. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io). + +Although this library is part of the [Utopia Framework](https://github.com/utopia-php/framework) project, it is completely **dependency-free** and can be used as standalone with any other PHP project or framework. + +## Getting Started + +Install using composer: +```bash +composer require utopia-php/emails +``` + +```php +get(); // user@example.com +$email->getLocal(); // user +$email->getDomain(); // example.com +$email->getLocalOnly(); // user +$email->getDomainOnly(); // example.com +$email->isValid(); // true +$email->hasValidLocal(); // true +$email->hasValidDomain(); // true + +// Email classification +$email->isDisposable(); // false +$email->isFree(); // false +$email->isCorporate(); // true + +// Domain analysis +$email->getProvider(); // example.com +$email->getSubdomain(); // '' +$email->hasSubdomain(); // false + +// Email with subdomain +$email = new Email('user@mail.example.com'); + +$email->get(); // user@mail.example.com +$email->getLocal(); // user +$email->getDomain(); // mail.example.com +$email->getProvider(); // example.com +$email->getSubdomain(); // mail +$email->hasSubdomain(); // true + +// Email formatting +$email->getFormatted('full'); // user@mail.example.com +$email->getFormatted('local'); // user +$email->getFormatted('domain'); // mail.example.com +$email->getFormatted('provider'); // example.com +$email->getFormatted('subdomain'); // mail + +// Email normalization +$email = new Email(' USER@EXAMPLE.COM '); +$email->get(); // user@example.com +$email->normalize(); // user@example.com + +``` + +## Library API + +### Email Class + +* **get()** - Return full email address. +* **getLocal()** - Return local part (before @). +* **getDomain()** - Return domain part (after @). +* **getLocalOnly()** - Return email without domain part (local only). +* **getDomainOnly()** - Return email without local part (domain only). +* **isValid()** - Check if email is valid format. +* **hasValidLocal()** - Check if email has valid local part. +* **hasValidDomain()** - Check if email has valid domain part. +* **isDisposable()** - Check if email is from a disposable email service. +* **isFree()** - Check if email is from a free email service. +* **isCorporate()** - Check if email is from a corporate domain. +* **getProvider()** - Get email provider (domain without subdomain). +* **getSubdomain()** - Get email subdomain (if any). +* **hasSubdomain()** - Check if email has subdomain. +* **normalize()** - Normalize email address (remove extra spaces, convert to lowercase). +* **getFormatted(string $format)** - Get email in different formats ('full', 'local', 'domain', 'provider', 'subdomain'). + +## Using the Validators + +```php +isValid('user@example.com'); // true +$basicValidator->isValid('invalid-email'); // false + +// Advanced email validation +$validator = new EmailAddress(); +$validator->isValid('user@example.com'); // true +$validator->isValid('invalid-email'); // false + +// Domain validation +$domainValidator = new EmailDomain(); +$domainValidator->isValid('user@example.com'); // true +$domainValidator->isValid('user@example..com'); // false + +// Local part validation +$localValidator = new EmailLocal(); +$localValidator->isValid('user@example.com'); // true +$localValidator->isValid('user..name@example.com'); // false + +// Non-disposable email validation +$notDisposableValidator = new EmailNotDisposable(); +$notDisposableValidator->isValid('user@example.com'); // true +$notDisposableValidator->isValid('user@10minutemail.com'); // false + +// Corporate email validation +$corporateValidator = new EmailCorporate(); +$corporateValidator->isValid('user@company.com'); // true +$corporateValidator->isValid('user@gmail.com'); // false + +``` + +## Library Validators API + +* **EmailBasic** - Basic email validation using PHP's filter_var function. +* **EmailAddress** - Advanced email validation with custom rules. +* **EmailDomain** - Validates that an email address has a valid domain. +* **EmailLocal** - Validates that an email address has a valid local part. +* **EmailNotDisposable** - Validates that an email address is not from a disposable email service. +* **EmailCorporate** - Validates that an email address is from a corporate domain (not free or disposable). + +## Email Classification + +The library automatically classifies emails into three categories: + +### Free Email Services +Common free email providers like Gmail, Yahoo, Hotmail, Outlook, etc. + +### Disposable Email Services +Temporary email services like 10minutemail, GuerrillaMail, Mailinator, etc. + +### Corporate Email Services +All other email addresses that are not classified as free or disposable. + +## Supported Email Formats + +The library supports various email formats including: + +- Basic: `user@example.com` +- With dots: `user.name@example.com` +- With plus: `user+tag@example.com` +- With hyphens: `user-name@example.com` +- With underscores: `user_name@example.com` +- With numbers: `user123@example123.com` +- With subdomains: `user@mail.example.com` +- With multiple subdomains: `user@mail.sub.example.com` + +## Validation Rules + +### Local Part (before @) +- Maximum 64 characters +- Can contain letters, numbers, dots, underscores, hyphens, and plus signs +- Cannot start or end with a dot +- Cannot contain consecutive dots + +### Domain Part (after @) +- Maximum 253 characters +- Must contain at least one dot +- Must have a valid TLD (at least 2 characters) +- Can contain letters, numbers, dots, and hyphens +- Cannot start or end with a dot or hyphen +- Cannot contain consecutive dots or hyphens + +## Data Management & Import System + +The library uses external data files to classify email domains as free or disposable. These files are located in the `data/` directory: + +- `data/free-domains.php` - List of known free email service providers +- `data/disposable-domains.php` - List of known disposable/temporary email services +- `data/sources.php` - Configuration for import sources +- `data/free-domains-manual.php` - Manually managed free email domains +- `data/disposable-domains-manual.php` - Manually managed disposable email domains + +### Import System + +The library includes a comprehensive import system that can automatically update domain lists from multiple sources. + +#### Quick Start + +```bash +# Install dependencies +composer install + +# Show current statistics +php import.php stats + +# Update all domains (preview only) +php import.php all + +# Update and commit changes +php import.php all --commit=true +``` + +#### Available Commands + +**Update All Domains** +```bash +# Preview changes without committing +php import.php all + +# Force update and commit changes +php import.php all --force=true --commit=true +``` + +**Update Disposable Domains Only** +```bash +# Update from all sources +php import.php disposable --commit=true + +# Update from specific source +php import.php disposable --source=martenson --commit=true + +# Force update even if no changes detected +php import.php disposable --force=true --commit=true +``` + +**Update Free Domains Only** +```bash +# Update free domains +php import.php free --commit=true + +# Update from specific source +php import.php free --source=manual --commit=true +``` + +**Show Statistics** +```bash +# Display current domain statistics +php import.php stats +``` + +#### Composer Scripts + +For convenience, you can also use composer scripts: + +```bash +# Using composer scripts +composer run import:all +composer run import:disposable +composer run import:free +composer run import:stats +``` + +#### Import Sources + +**Disposable Email Sources:** +- Manual Disposable Email Domains (configurable) +- Martenson Disposable Email Domains +- Disposable Email Domains +- Wes Bos Burner Email Providers +- 7c FakeFilter Domains +- Adam Loving Temporary Email Domains + +**Free Email Sources:** +- Manual Free Email Domains (configurable) + +#### Features + +- **Multiple Sources**: Support for 5+ disposable email domain sources +- **Manual Configuration**: Ability to manually manage both free and disposable email domains +- **Domain Validation**: Built-in domain validation using Utopia Domains +- **Statistics & Analysis**: Detailed domain statistics and TLD analysis +- **Deduplication**: Automatic removal of duplicate domains +- **Error Handling**: Robust error handling with graceful fallbacks + +#### Manual Domain Management + +You can manually edit the domain files to add or remove domains: + +**Free Email Domains** - Edit `data/free-domains-manual.php`: +```php +=8.0", + "utopia-php/framework": "0.33.*", + "utopia-php/cli": "^0.15", + "utopia-php/domains": "^0.1", + "utopia-php/fetch": "^0.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "laravel/pint": "1.2.*", + "phpstan/phpstan": "1.9.x-dev" + }, + "minimum-stability": "stable", + "config": { + "allow-plugins": { + "php-http/discovery": false, + "tbachert/spi": false + } + } +} + diff --git a/data/disposable-domains-manual.php b/data/disposable-domains-manual.php new file mode 100644 index 0000000..3c1e6ab --- /dev/null +++ b/data/disposable-domains-manual.php @@ -0,0 +1,14 @@ + [ + 'manual' => [ + 'name' => 'Manual Disposable Email Domains', + 'url' => null, + 'enabled' => true, + 'description' => 'Manually managed disposable email domains', + 'configFile' => 'disposable-domains-manual.php' + ], + 'martenson' => [ + 'name' => 'Martenson Disposable Email Domains', + 'url' => 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf', + 'enabled' => true, + 'description' => 'Comprehensive list of disposable email domains from Martenson repository' + ], + 'disposable' => [ + 'name' => 'Disposable Email Domains', + 'url' => 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt', + 'enabled' => true, + 'description' => 'Popular disposable email domains list' + ], + 'wesbos' => [ + 'name' => 'Wes Bos Burner Email Providers', + 'url' => 'https://raw.githubusercontent.com/wesbos/burner-email-providers/refs/heads/master/emails.txt', + 'enabled' => true, + 'description' => 'Burner email providers from Wes Bos repository' + ], + 'fakefilter' => [ + 'name' => '7c FakeFilter Domains', + 'url' => 'https://raw.githubusercontent.com/7c/fakefilter/main/txt/data.txt', + 'enabled' => true, + 'description' => 'Fake email domains from 7c FakeFilter' + ], + 'adamloving' => [ + 'name' => 'Adam Loving Temporary Email Domains', + 'url' => 'https://gist.githubusercontent.com/adamloving/4401361/raw/e81212c3caecb54b87ced6392e0a0de2b6466287/temporary-email-address-domains', + 'enabled' => true, + 'description' => 'Temporary email domains from Adam Loving gist' + ] + ], + 'free' => [ + 'manual' => [ + 'name' => 'Manual Free Email Domains', + 'url' => null, + 'enabled' => true, + 'description' => 'Manually managed free email domains', + 'configFile' => 'free-domains-manual.php' + ] + ] +]; diff --git a/import.php b/import.php new file mode 100644 index 0000000..94a6dc7 --- /dev/null +++ b/import.php @@ -0,0 +1,846 @@ + [ + 'name' => 'Manual Disposable Email Domains', + 'url' => null, + 'configFile' => CONFIG_DIR . '/disposable-domains-manual.php' + ], + 'martenson' => [ + 'name' => 'Martenson Disposable Email Domains', + 'url' => 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf', + 'configFile' => CONFIG_DIR . '/disposable-domains-martenson.php' + ], + 'disposable' => [ + 'name' => 'Disposable Email Domains', + 'url' => 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt', + 'configFile' => CONFIG_DIR . '/disposable-domains-disposable.php' + ], + 'wesbos' => [ + 'name' => 'Wes Bos Burner Email Providers', + 'url' => 'https://raw.githubusercontent.com/wesbos/burner-email-providers/refs/heads/master/emails.txt', + 'configFile' => CONFIG_DIR . '/disposable-domains-wesbos.php' + ], + 'fakefilter' => [ + 'name' => '7c FakeFilter Domains', + 'url' => 'https://raw.githubusercontent.com/7c/fakefilter/main/txt/data.txt', + 'configFile' => CONFIG_DIR . '/disposable-domains-fakefilter.php' + ], + 'adamloving' => [ + 'name' => 'Adam Loving Temporary Email Domains', + 'url' => 'https://gist.githubusercontent.com/adamloving/4401361/raw/e81212c3caecb54b87ced6392e0a0de2b6466287/temporary-email-address-domains', + 'configFile' => CONFIG_DIR . '/disposable-domains-adamloving.php' + ] +]; + +/** + * Source configurations for free email domains + */ +const FREE_SOURCES = [ + 'manual' => [ + 'name' => 'Manual Free Email Domains', + 'url' => null, + 'configFile' => CONFIG_DIR . '/free-domains-manual.php' + ] +]; + +/** + * Update disposable email domains from multiple sources + */ +function updateDisposableDomains(bool $commit, bool $force, string $source): void +{ + Console::title('Disposable Email Domains Update'); + Console::success('Utopia Emails disposable domains update process has started'); + + try { + $sources = $source ? [$source => DISPOSABLE_SOURCES[$source] ?? null] : DISPOSABLE_SOURCES; + $sources = array_filter($sources); // Remove null values + + if (empty($sources)) { + Console::error('No valid sources found'); + Console::exit(1); + } + + $allDomains = fetchAllSources($sources, 'disposable'); + $currentDomains = loadCurrentConfig('disposable-domains.php'); + + if (empty($allDomains)) { + Console::error('Failed to fetch disposable email domains or list is empty'); + Console::exit(1); + } + + Console::info('Fetched ' . count($allDomains) . ' disposable email domains from all sources'); + + showDomainStatistics($allDomains); + + if (!$force && isConfigUpToDate($currentDomains, $allDomains)) { + Console::success('Disposable email domains are already up to date'); + Console::exit(0); + } + + Console::info('Changes detected:'); + Console::info('- Previous domains count: ' . count($currentDomains)); + Console::info('- New domains count: ' . count($allDomains)); + + if ($commit) { + saveConfig('disposable-domains.php', $allDomains, 'Disposable Email Domains'); + Console::success('Successfully updated disposable email domains configuration'); + } else { + Console::warning('Changes not yet committed to config file. Please provide --commit=true argument to commit changes.'); + Console::info('Preview of changes:'); + showPreview($currentDomains, $allDomains); + } + + } catch (\Throwable $e) { + Console::error('Error updating disposable email domains: ' . $e->getMessage()); + Console::exit(1); + } +} + +/** + * Update free email domains from multiple sources + */ +function updateFreeDomains(bool $commit, bool $force, string $source): void +{ + Console::title('Free Email Domains Update'); + Console::success('Utopia Emails free domains update process has started'); + + try { + $sources = $source ? [$source => FREE_SOURCES[$source] ?? null] : FREE_SOURCES; + $sources = array_filter($sources); // Remove null values + + if (empty($sources)) { + Console::error('No valid sources found'); + Console::exit(1); + } + + $allDomains = fetchAllSources($sources, 'free'); + $currentDomains = loadCurrentConfig('free-domains.php'); + + if (empty($allDomains)) { + Console::error('Failed to fetch free email domains or list is empty'); + Console::exit(1); + } + + Console::info('Fetched ' . count($allDomains) . ' free email domains from all sources'); + + showDomainStatistics($allDomains); + + if (!$force && isConfigUpToDate($currentDomains, $allDomains)) { + Console::success('Free email domains are already up to date'); + Console::exit(0); + } + + Console::info('Changes detected:'); + Console::info('- Previous domains count: ' . count($currentDomains)); + Console::info('- New domains count: ' . count($allDomains)); + + if ($commit) { + saveConfig('free-domains.php', $allDomains, 'Free Email Domains'); + Console::success('Successfully updated free email domains configuration'); + } else { + Console::warning('Changes not yet committed to config file. Please provide --commit=true argument to commit changes.'); + Console::info('Preview of changes:'); + showPreview($currentDomains, $allDomains); + } + + } catch (\Throwable $e) { + Console::error('Error updating free email domains: ' . $e->getMessage()); + Console::exit(1); + } +} + +/** + * Update all email domains from all sources + */ +function updateAllDomains(bool $commit, bool $force): void +{ + Console::title('All Email Domains Update'); + Console::success('Utopia Emails all domains update process has started'); + + try { + // Update disposable domains + updateDisposableDomains($commit, $force, ''); + + Console::info(''); + + // Update free domains + updateFreeDomains($commit, $force, ''); + + Console::success('Successfully updated all email domains'); + + } catch (\Throwable $e) { + Console::error('Error updating all email domains: ' . $e->getMessage()); + Console::exit(1); + } +} + +/** + * Show statistics about current domain lists + */ +function showStats(): void +{ + Console::title('Email Domains Statistics'); + + try { + $disposableDomains = loadCurrentConfig('disposable-domains.php'); + $freeDomains = loadCurrentConfig('free-domains.php'); + + Console::info('Current Domain Statistics:'); + Console::info('├─ Disposable domains: ' . count($disposableDomains)); + Console::info('└─ Free domains: ' . count($freeDomains)); + + if (!empty($disposableDomains)) { + Console::info(''); + Console::info('Disposable Domains Analysis:'); + showDomainStatistics($disposableDomains); + } + + if (!empty($freeDomains)) { + Console::info(''); + Console::info('Free Domains Analysis:'); + showDomainStatistics($freeDomains); + } + + } catch (\Throwable $e) { + Console::error('Error showing statistics: ' . $e->getMessage()); + Console::exit(1); + } +} + +/** + * Fetch domains from all sources + */ +function fetchAllSources(array $sources, string $type): array +{ + $allDomains = []; + $totalSources = count($sources); + $processedSources = 0; + $totalFetched = 0; + + Console::info("Fetching from {$totalSources} sources..."); + + foreach ($sources as $sourceKey => $sourceConfig) { + $processedSources++; + Console::info("[{$processedSources}/{$totalSources}] Processing {$sourceConfig['name']}..."); + + try { + $domains = fetchSource($sourceKey, $sourceConfig, $type); + $totalFetched += count($domains); + + // Add domains to the collection, avoiding duplicates + foreach ($domains as $domain) { + $allDomains[$domain] = true; // Use associative array to avoid duplicates + } + + Console::info("✓ Fetched " . count($domains) . " domains from {$sourceConfig['name']}"); + } catch (\Exception $e) { + Console::warning("⚠ Failed to fetch from {$sourceConfig['name']}: " . $e->getMessage()); + // Continue with other sources even if one fails + } + } + + // Convert back to indexed array and sort + $uniqueDomains = array_keys($allDomains); + sort($uniqueDomains); + + $duplicatesRemoved = $totalFetched - count($uniqueDomains); + Console::info("Total domains fetched: {$totalFetched}"); + Console::info("Duplicates removed: {$duplicatesRemoved}"); + Console::info("Total unique domains after merging all sources: " . count($uniqueDomains)); + + return $uniqueDomains; +} + +/** + * Fetch domains from a specific source + */ +function fetchSource(string $sourceKey, array $sourceConfig, string $type): array +{ + if ($type === 'disposable') { + switch ($sourceKey) { + case 'manual': + return loadManualDisposableDomains($sourceConfig); + case 'martenson': + return fetchMartensonDomains($sourceConfig); + case 'disposable': + return fetchDisposableDomains($sourceConfig); + case 'wesbos': + return fetchWesbosDomains($sourceConfig); + case 'fakefilter': + return fetchFakeFilterDomains($sourceConfig); + case 'adamloving': + return fetchAdamLovingDomains($sourceConfig); + default: + throw new \Exception("Unknown disposable source: {$sourceKey}"); + } + } elseif ($type === 'free') { + switch ($sourceKey) { + case 'manual': + return loadManualFreeDomains($sourceConfig); + default: + throw new \Exception("Unknown free source: {$sourceKey}"); + } + } + + throw new \Exception("Unknown type: {$type}"); +} + +/** + * Fetch domains from Martenson repository + */ +function fetchMartensonDomains(array $sourceConfig): array +{ + try { + $client = new \Utopia\Fetch\Client(); + + $response = $client->fetch( + url: $sourceConfig['url'], + method: \Utopia\Fetch\Client::METHOD_GET + ); + + if ($response->getStatusCode() !== 200) { + throw new \Exception('HTTP ' . $response->getStatusCode()); + } + + $content = $response->getBody(); + + } catch (\Exception $e) { + throw new \Exception('Network error: ' . $e->getMessage()); + } + + $domains = []; + $lines = explode("\n", $content); + $processed = 0; + $valid = 0; + + foreach ($lines as $line) { + $line = trim($line); + $processed++; + + if (empty($line) || str_starts_with($line, '#')) { + continue; + } + + if (isValidDomain($line)) { + $domains[] = strtolower($line); + $valid++; + } + } + + Console::info(" Processed {$processed} lines, found {$valid} valid domains"); + + return $domains; +} + +/** + * Fetch domains from Disposable repository + */ +function fetchDisposableDomains(array $sourceConfig): array +{ + try { + $client = new \Utopia\Fetch\Client(); + + $response = $client->fetch( + url: $sourceConfig['url'], + method: \Utopia\Fetch\Client::METHOD_GET + ); + + if ($response->getStatusCode() !== 200) { + throw new \Exception('HTTP ' . $response->getStatusCode()); + } + + $content = $response->getBody(); + + } catch (\Exception $e) { + throw new \Exception('Network error: ' . $e->getMessage()); + } + + $domains = []; + $processed = 0; + $valid = 0; + + $domainList = preg_split('/\s+/', trim($content)); + + foreach ($domainList as $domain) { + $domain = trim($domain); + $processed++; + + if (empty($domain)) { + continue; + } + + if (isValidDomain($domain)) { + $domains[] = strtolower($domain); + $valid++; + } + } + + Console::info(" Processed {$processed} domains, found {$valid} valid domains"); + + return $domains; +} + +/** + * Fetch domains from Wes Bos repository + */ +function fetchWesbosDomains(array $sourceConfig): array +{ + try { + $client = new \Utopia\Fetch\Client(); + + $response = $client->fetch( + url: $sourceConfig['url'], + method: \Utopia\Fetch\Client::METHOD_GET + ); + + if ($response->getStatusCode() !== 200) { + throw new \Exception('HTTP ' . $response->getStatusCode()); + } + + $content = $response->getBody(); + + } catch (\Exception $e) { + throw new \Exception('Network error: ' . $e->getMessage()); + } + + $domains = []; + $processed = 0; + $valid = 0; + + $domainList = preg_split('/\s+/', trim($content)); + + foreach ($domainList as $domain) { + $domain = trim($domain); + $processed++; + + if (empty($domain)) { + continue; + } + + if (isValidDomain($domain)) { + $domains[] = strtolower($domain); + $valid++; + } + } + + Console::info(" Processed {$processed} domains, found {$valid} valid domains"); + + return $domains; +} + +/** + * Fetch domains from FakeFilter repository + */ +function fetchFakeFilterDomains(array $sourceConfig): array +{ + try { + $client = new \Utopia\Fetch\Client(); + + $response = $client->fetch( + url: $sourceConfig['url'], + method: \Utopia\Fetch\Client::METHOD_GET + ); + + if ($response->getStatusCode() !== 200) { + throw new \Exception('HTTP ' . $response->getStatusCode()); + } + + $content = $response->getBody(); + + } catch (\Exception $e) { + throw new \Exception('Network error: ' . $e->getMessage()); + } + + $domains = []; + $lines = explode("\n", $content); + $processed = 0; + $valid = 0; + + foreach ($lines as $line) { + $line = trim($line); + $processed++; + + if (empty($line) || str_starts_with($line, '#')) { + continue; + } + + if (isValidDomain($line)) { + $domains[] = strtolower($line); + $valid++; + } + } + + Console::info(" Processed {$processed} lines, found {$valid} valid domains"); + + return $domains; +} + +/** + * Fetch domains from Adam Loving gist + */ +function fetchAdamLovingDomains(array $sourceConfig): array +{ + try { + $client = new \Utopia\Fetch\Client(); + + $response = $client->fetch( + url: $sourceConfig['url'], + method: \Utopia\Fetch\Client::METHOD_GET + ); + + if ($response->getStatusCode() !== 200) { + throw new \Exception('HTTP ' . $response->getStatusCode()); + } + + $content = $response->getBody(); + + } catch (\Exception $e) { + throw new \Exception('Network error: ' . $e->getMessage()); + } + + $domains = []; + $processed = 0; + $valid = 0; + + $domainList = preg_split('/\s+/', trim($content)); + + foreach ($domainList as $domain) { + $domain = trim($domain); + $processed++; + + if (empty($domain)) { + continue; + } + + if (isValidDomain($domain)) { + $domains[] = strtolower($domain); + $valid++; + } + } + + Console::info(" Processed {$processed} domains, found {$valid} valid domains"); + + return $domains; +} + +/** + * Load manual disposable domains + */ +function loadManualDisposableDomains(array $sourceConfig): array +{ + if (!file_exists($sourceConfig['configFile'])) { + Console::info(" Manual config file not found, creating empty list"); + return []; + } + + $domains = include $sourceConfig['configFile']; + Console::info(" Loaded " . count($domains) . " domains from manual config"); + + return $domains; +} + +/** + * Load manual free domains + */ +function loadManualFreeDomains(array $sourceConfig): array +{ + if (!file_exists($sourceConfig['configFile'])) { + Console::info(" Manual config file not found, creating empty list"); + return []; + } + + $domains = include $sourceConfig['configFile']; + Console::info(" Loaded " . count($domains) . " domains from manual config"); + + return $domains; +} + +/** + * Validate if a domain is a valid email domain + */ +function isValidDomain(string $domain): bool +{ + if (empty($domain)) { + return false; + } + + try { + $domainObj = new \Utopia\Domains\Domain($domain); + + if ($domainObj->isTest()) { + return false; + } + + $name = $domainObj->getName(); + $tld = $domainObj->getTLD(); + + if (empty($name) || empty($tld)) { + return false; + } + + return true; + + } catch (\Exception $e) { + return false; + } +} + +/** + * Load current configuration + */ +function loadCurrentConfig(string $filename): array +{ + $filepath = CONFIG_DIR . '/' . $filename; + + if (!file_exists($filepath)) { + return []; + } + + return include $filepath; +} + +/** + * Check if configuration is up to date + */ +function isConfigUpToDate(array $currentDomains, array $newDomains): bool +{ + return $currentDomains == $newDomains; +} + +/** + * Save configuration to file + */ +function saveConfig(string $filename, array $domains, string $description): void +{ + $configFile = CONFIG_DIR . '/' . $filename; + $lastUpdated = date('Y-m-d H:i:s'); + + // Sort domains for consistent output + sort($domains); + + $configContent = "getTLD(); + $tldStats[$tld] = ($tldStats[$tld] ?? 0) + 1; + + if ($domainObj->isKnown()) { + $knownDomains++; + if ($domainObj->isICANN()) { + $icannDomains++; + } elseif ($domainObj->isPrivate()) { + $privateDomains++; + } + } else { + $unknownDomains++; + } + } catch (\Exception $e) { + // Skip invalid domains + } + } + + arsort($tldStats); + $topTlds = array_slice($tldStats, 0, 10, true); + + Console::info('Domain Statistics:'); + Console::info('├─ Known domains: ' . $knownDomains . ' (' . round(($knownDomains / count($domains)) * 100, 1) . '%)'); + Console::info('├─ ICANN domains: ' . $icannDomains . ' (' . round(($icannDomains / count($domains)) * 100, 1) . '%)'); + Console::info('├─ Private domains: ' . $privateDomains . ' (' . round(($privateDomains / count($domains)) * 100, 1) . '%)'); + Console::info('└─ Unknown domains: ' . $unknownDomains . ' (' . round(($unknownDomains / count($domains)) * 100, 1) . '%)'); + + Console::info('Top 10 TLDs:'); + foreach ($topTlds as $tld => $count) { + Console::info(" ├─ .{$tld}: {$count} domains"); + } +} + +/** + * Show preview of changes + */ +function showPreview(array $currentDomains, array $newDomains): void +{ + $added = array_diff($newDomains, $currentDomains); + $removed = array_diff($currentDomains, $newDomains); + + if (!empty($added)) { + Console::info('Domains to be added (' . count($added) . '):'); + foreach (array_slice($added, 0, 10) as $domain) { + Console::info(" ├─ + {$domain}"); + } + if (count($added) > 10) { + Console::info(' └─ ... and ' . (count($added) - 10) . ' more'); + } + } + + if (!empty($removed)) { + Console::info('Domains to be removed (' . count($removed) . '):'); + foreach (array_slice($removed, 0, 10) as $domain) { + Console::info(" ├─ - {$domain}"); + } + if (count($removed) > 10) { + Console::info(' └─ ... and ' . (count($removed) - 10) . ' more'); + } + } +} + +// Setup CLI +$cli = new CLI(); + +// Disposable domains command +$cli + ->task('disposable') + ->desc('Update disposable email domains from multiple sources') + ->param('commit', false, new Boolean(true), 'If set will commit changes to config file. Default is false.', true) + ->param('force', false, new Boolean(true), 'Force update even if no changes detected. Default is false.', true) + ->param('source', '', new Text(100), 'Specific source to update (optional). Leave empty to update all sources.', true) + ->action(function(bool $commit, bool $force, string $source) { + updateDisposableDomains($commit, $force, $source); + }); + +// Free domains command +$cli + ->task('free') + ->desc('Update free email domains from multiple sources') + ->param('commit', false, new Boolean(true), 'If set will commit changes to config file. Default is false.', true) + ->param('force', false, new Boolean(true), 'Force update even if no changes detected. Default is false.', true) + ->param('source', '', new Text(100), 'Specific source to update (optional). Leave empty to update all sources.', true) + ->action(function(bool $commit, bool $force, string $source) { + updateFreeDomains($commit, $force, $source); + }); + +// All domains command +$cli + ->task('all') + ->desc('Update both disposable and free email domains from all sources') + ->param('commit', false, new Boolean(true), 'If set will commit changes to config file. Default is false.', true) + ->param('force', false, new Boolean(true), 'Force update even if no changes detected. Default is false.', true) + ->action(function(bool $commit, bool $force) { + updateAllDomains($commit, $force); + }); + +// Stats command +$cli + ->task('stats') + ->desc('Show statistics about current domain lists') + ->action(function() { + showStats(); + }); + +// Run the CLI +$cli->run(); \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..7a10c7a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + ./tests/ + + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..0fe772f --- /dev/null +++ b/pint.json @@ -0,0 +1,10 @@ +{ + "preset": "laravel", + "rules": { + "simplified_null_return": true, + "blank_line_before_statement": { + "statements": ["break", "continue", "declare", "return", "throw", "try"] + } + } +} + diff --git a/src/Emails/Email.php b/src/Emails/Email.php new file mode 100644 index 0000000..0b6f249 --- /dev/null +++ b/src/Emails/Email.php @@ -0,0 +1,280 @@ +email = \mb_strtolower(\trim($email)); + + if (empty($this->email)) { + throw new Exception("Email address cannot be empty"); + } + + $this->parts = \explode('@', $this->email); + + if (count($this->parts) !== 2) { + throw new Exception("'{$email}' must be a valid email address"); + } + + $this->local = $this->parts[0]; + $this->domain = $this->parts[1]; + + if (empty($this->local) || empty($this->domain)) { + throw new Exception("'{$email}' must be a valid email address"); + } + } + + /** + * Return full email address + */ + public function get(): string + { + return $this->email; + } + + /** + * Return local part (before @) + */ + public function getLocal(): string + { + return $this->local; + } + + /** + * Return domain part (after @) + */ + public function getDomain(): string + { + return $this->domain; + } + + /** + * Return email without local part (domain only) + */ + public function getDomainOnly(): string + { + return $this->domain; + } + + /** + * Return email without domain part (local only) + */ + public function getLocalOnly(): string + { + return $this->local; + } + + /** + * Check if email is valid format + */ + public function isValid(): bool + { + return filter_var($this->email, FILTER_VALIDATE_EMAIL) !== false; + } + + /** + * Check if email has valid local part + */ + public function hasValidLocal(): bool + { + // Check local part length + if (mb_strlen($this->local) > self::LOCAL_MAX_LENGTH) { + return false; + } + + // Check for valid characters in local part + if (!preg_match('/^[a-zA-Z0-9._+-]+$/', $this->local)) { + return false; + } + + // Check for consecutive dots + if (strpos($this->local, '..') !== false) { + return false; + } + + // Check if starts or ends with dot + if (str_starts_with($this->local, '.') || str_ends_with($this->local, '.')) { + return false; + } + + return true; + } + + /** + * Check if email has valid domain part + */ + public function hasValidDomain(): bool + { + // Check domain part length + if (mb_strlen($this->domain) > self::DOMAIN_MAX_LENGTH) { + return false; + } + + // Check for valid domain format using filter_var + if (!filter_var('test@' . $this->domain, FILTER_VALIDATE_EMAIL)) { + return false; + } + + return true; + } + + /** + * Check if email is from a disposable email service + */ + public function isDisposable(): bool + { + if (self::$disposableDomains === null) { + self::$disposableDomains = include __DIR__ . '/../../data/disposable-domains.php'; + } + + return in_array($this->domain, self::$disposableDomains); + } + + /** + * Check if email is from a free email service + */ + public function isFree(): bool + { + if (self::$freeDomains === null) { + self::$freeDomains = include __DIR__ . '/../../data/free-domains.php'; + } + + return in_array($this->domain, self::$freeDomains); + } + + /** + * Check if email is from a corporate domain + */ + public function isCorporate(): bool + { + return !$this->isFree() && !$this->isDisposable(); + } + + /** + * Get email provider (domain without subdomain) + */ + public function getProvider(): string + { + $domainParts = explode('.', $this->domain); + + if (count($domainParts) < 2) { + return $this->domain; + } + + // For domains like mail.company.com, return company.com + if (count($domainParts) > 2) { + return implode('.', array_slice($domainParts, -2)); + } + + return $this->domain; + } + + /** + * Get email subdomain (if any) + */ + public function getSubdomain(): string + { + $domainParts = explode('.', $this->domain); + + if (count($domainParts) <= 2) { + return ''; + } + + return implode('.', array_slice($domainParts, 0, -2)); + } + + /** + * Check if email has subdomain + */ + public function hasSubdomain(): bool + { + return !empty($this->getSubdomain()); + } + + /** + * Normalize email address (remove extra spaces, convert to lowercase) + */ + public function normalize(): string + { + return $this->email; + } + + /** + * Get email in different formats + */ + public function getFormatted(string $format = 'full'): string + { + switch ($format) { + case 'local': + return $this->local; + case 'domain': + return $this->domain; + case 'provider': + return $this->getProvider(); + case 'subdomain': + return $this->getSubdomain(); + case 'full': + default: + return $this->email; + } + } +} diff --git a/src/Emails/Validator/Email.php b/src/Emails/Validator/Email.php new file mode 100644 index 0000000..7793d14 --- /dev/null +++ b/src/Emails/Validator/Email.php @@ -0,0 +1,72 @@ +isValid(); + } catch (\Exception $e) { + return false; + } + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} diff --git a/src/Emails/Validator/EmailCorporate.php b/src/Emails/Validator/EmailCorporate.php new file mode 100644 index 0000000..67e43c3 --- /dev/null +++ b/src/Emails/Validator/EmailCorporate.php @@ -0,0 +1,73 @@ +isValid() && $email->isCorporate(); + } catch (\Exception $e) { + return false; + } + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} + diff --git a/src/Emails/Validator/EmailDomain.php b/src/Emails/Validator/EmailDomain.php new file mode 100644 index 0000000..0be9164 --- /dev/null +++ b/src/Emails/Validator/EmailDomain.php @@ -0,0 +1,73 @@ +isValid() && $email->hasValidDomain(); + } catch (\Exception $e) { + return false; + } + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} + diff --git a/src/Emails/Validator/EmailLocal.php b/src/Emails/Validator/EmailLocal.php new file mode 100644 index 0000000..7d586cd --- /dev/null +++ b/src/Emails/Validator/EmailLocal.php @@ -0,0 +1,73 @@ +isValid() && $email->hasValidLocal(); + } catch (\Exception $e) { + return false; + } + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} + diff --git a/src/Emails/Validator/EmailNotDisposable.php b/src/Emails/Validator/EmailNotDisposable.php new file mode 100644 index 0000000..fa4ed3b --- /dev/null +++ b/src/Emails/Validator/EmailNotDisposable.php @@ -0,0 +1,73 @@ +isValid() && !$email->isDisposable(); + } catch (\Exception $e) { + return false; + } + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return false; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_STRING; + } +} + diff --git a/test-import.php b/test-import.php new file mode 100644 index 0000000..a953633 --- /dev/null +++ b/test-import.php @@ -0,0 +1,133 @@ +domain = $domain; + } + + public function isTest() { return false; } + public function getName() { + $parts = explode('.', $this->domain); + return $parts[0]; + } + public function getTLD() { + $parts = explode('.', $this->domain); + return end($parts); + } + public function isKnown() { return true; } + public function isICANN() { return true; } + public function isPrivate() { return false; } +} + +// Test domain validation +function isValidDomain($domain) { + if (empty($domain)) { + return false; + } + + try { + $domainObj = new MockDomain($domain); + + if ($domainObj->isTest()) { + return false; + } + + $name = $domainObj->getName(); + $tld = $domainObj->getTLD(); + + if (empty($name) || empty($tld)) { + return false; + } + + return true; + + } catch (Exception $e) { + return false; + } +} + +// Test the import system +echo "Testing Email Domains Import System\n"; +echo "===================================\n\n"; + +// Test domain validation +$testDomains = [ + 'gmail.com' => true, + 'test.com' => true, + 'invalid' => false, + '' => false, + 'subdomain.example.com' => true, + 'test' => false, +]; + +MockCLI::info('Testing domain validation...'); +foreach ($testDomains as $domain => $expected) { + $result = isValidDomain($domain); + $status = $result === $expected ? 'PASS' : 'FAIL'; + echo " $status: '$domain' -> " . ($result ? 'valid' : 'invalid') . "\n"; +} + +// Test source configuration loading +MockCLI::info('Testing source configuration...'); +if (file_exists(__DIR__ . '/data/sources.php')) { + $sources = include __DIR__ . '/data/sources.php'; + echo " ✓ Sources configuration loaded\n"; + echo " - Disposable sources: " . count($sources['disposable']) . "\n"; + echo " - Free sources: " . count($sources['free']) . "\n"; +} else { + echo " ✗ Sources configuration not found\n"; +} + +// Test manual free domains +MockCLI::info('Testing manual free domains...'); +if (file_exists(__DIR__ . '/data/free-domains-manual.php')) { + $domains = include __DIR__ . '/data/free-domains-manual.php'; + echo " ✓ Manual free domains loaded: " . count($domains) . " domains\n"; +} else { + echo " ✗ Manual free domains not found\n"; +} + +// Test manual disposable domains +MockCLI::info('Testing manual disposable domains...'); +if (file_exists(__DIR__ . '/data/disposable-domains-manual.php')) { + $domains = include __DIR__ . '/data/disposable-domains-manual.php'; + echo " ✓ Manual disposable domains loaded: " . count($domains) . " domains\n"; +} else { + echo " ✗ Manual disposable domains not found\n"; +} + +// Test existing domain files +MockCLI::info('Testing existing domain files...'); +if (file_exists(__DIR__ . '/data/disposable-domains.php')) { + $domains = include __DIR__ . '/data/disposable-domains.php'; + echo " ✓ Disposable domains loaded: " . count($domains) . " domains\n"; +} else { + echo " ⚠ Disposable domains not found (will be created on first import)\n"; +} + +if (file_exists(__DIR__ . '/data/free-domains.php')) { + $domains = include __DIR__ . '/data/free-domains.php'; + echo " ✓ Free domains loaded: " . count($domains) . " domains\n"; +} else { + echo " ⚠ Free domains not found (will be created on first import)\n"; +} + +MockCLI::success('Test completed successfully!'); +MockCLI::info('To run the actual import, install dependencies and run: php import.php stats'); diff --git a/tests/EmailTest.php b/tests/EmailTest.php new file mode 100644 index 0000000..4233029 --- /dev/null +++ b/tests/EmailTest.php @@ -0,0 +1,384 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests; + +use Exception; +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Email; + +class EmailTest extends TestCase +{ + public function testValidEmail(): void + { + $email = new Email('test@example.com'); + + $this->assertEquals('test@example.com', $email->get()); + $this->assertEquals('test', $email->getLocal()); + $this->assertEquals('example.com', $email->getDomain()); + $this->assertEquals('example.com', $email->getDomainOnly()); + $this->assertEquals('test', $email->getLocalOnly()); + $this->assertEquals(true, $email->isValid()); + $this->assertEquals(true, $email->hasValidLocal()); + $this->assertEquals(true, $email->hasValidDomain()); + $this->assertEquals(false, $email->isDisposable()); + $this->assertEquals(false, $email->isFree()); + $this->assertEquals(true, $email->isCorporate()); + $this->assertEquals('example.com', $email->getProvider()); + $this->assertEquals('', $email->getSubdomain()); + $this->assertEquals(false, $email->hasSubdomain()); + $this->assertEquals('test@example.com', $email->normalize()); + } + + public function testEmailWithSubdomain(): void + { + $email = new Email('user@mail.example.com'); + + $this->assertEquals('user@mail.example.com', $email->get()); + $this->assertEquals('user', $email->getLocal()); + $this->assertEquals('mail.example.com', $email->getDomain()); + $this->assertEquals('example.com', $email->getProvider()); + $this->assertEquals('mail', $email->getSubdomain()); + $this->assertEquals(true, $email->hasSubdomain()); + } + + public function testGmailEmail(): void + { + $email = new Email('user@gmail.com'); + + $this->assertEquals('user@gmail.com', $email->get()); + $this->assertEquals('user', $email->getLocal()); + $this->assertEquals('gmail.com', $email->getDomain()); + $this->assertEquals(false, $email->isDisposable()); + $this->assertEquals(true, $email->isFree()); + $this->assertEquals(false, $email->isCorporate()); + $this->assertEquals('gmail.com', $email->getProvider()); + } + + public function testDisposableEmail(): void + { + $email = new Email('user@10minutemail.com'); + + $this->assertEquals('user@10minutemail.com', $email->get()); + $this->assertEquals('user', $email->getLocal()); + $this->assertEquals('10minutemail.com', $email->getDomain()); + $this->assertEquals(true, $email->isDisposable()); + $this->assertEquals(false, $email->isFree()); + $this->assertEquals(false, $email->isCorporate()); + } + + public function testEmailWithSpecialCharacters(): void + { + $email = new Email('user.name+tag@example.com'); + + $this->assertEquals('user.name+tag@example.com', $email->get()); + $this->assertEquals('user.name+tag', $email->getLocal()); + $this->assertEquals('example.com', $email->getDomain()); + $this->assertEquals(true, $email->isValid()); + $this->assertEquals(true, $email->hasValidLocal()); + $this->assertEquals(true, $email->hasValidDomain()); + } + + public function testEmailWithHyphens(): void + { + $email = new Email('user-name@example-domain.com'); + + $this->assertEquals('user-name@example-domain.com', $email->get()); + $this->assertEquals('user-name', $email->getLocal()); + $this->assertEquals('example-domain.com', $email->getDomain()); + $this->assertEquals(true, $email->isValid()); + $this->assertEquals(true, $email->hasValidLocal()); + $this->assertEquals(true, $email->hasValidDomain()); + } + + public function testEmailWithUnderscores(): void + { + $email = new Email('user_name@example.com'); + + $this->assertEquals('user_name@example.com', $email->get()); + $this->assertEquals('user_name', $email->getLocal()); + $this->assertEquals('example.com', $email->getDomain()); + $this->assertEquals(true, $email->isValid()); + $this->assertEquals(true, $email->hasValidLocal()); + $this->assertEquals(true, $email->hasValidDomain()); + } + + public function testEmailWithNumbers(): void + { + $email = new Email('user123@example123.com'); + + $this->assertEquals('user123@example123.com', $email->get()); + $this->assertEquals('user123', $email->getLocal()); + $this->assertEquals('example123.com', $email->getDomain()); + $this->assertEquals(true, $email->isValid()); + $this->assertEquals(true, $email->hasValidLocal()); + $this->assertEquals(true, $email->hasValidDomain()); + } + + public function testEmailWithMultipleDots(): void + { + $email = new Email('user.name.last@example.com'); + + $this->assertEquals('user.name.last@example.com', $email->get()); + $this->assertEquals('user.name.last', $email->getLocal()); + $this->assertEquals('example.com', $email->getDomain()); + $this->assertEquals(true, $email->isValid()); + $this->assertEquals(true, $email->hasValidLocal()); + $this->assertEquals(true, $email->hasValidDomain()); + } + + public function testEmailWithMultipleSubdomains(): void + { + $email = new Email('user@mail.sub.example.com'); + + $this->assertEquals('user@mail.sub.example.com', $email->get()); + $this->assertEquals('user', $email->getLocal()); + $this->assertEquals('mail.sub.example.com', $email->getDomain()); + $this->assertEquals('example.com', $email->getProvider()); + $this->assertEquals('mail.sub', $email->getSubdomain()); + $this->assertEquals(true, $email->hasSubdomain()); + } + + public function testEmailFormatted(): void + { + $email = new Email('user@mail.example.com'); + + $this->assertEquals('user@mail.example.com', $email->getFormatted('full')); + $this->assertEquals('user', $email->getFormatted('local')); + $this->assertEquals('mail.example.com', $email->getFormatted('domain')); + $this->assertEquals('example.com', $email->getFormatted('provider')); + $this->assertEquals('mail', $email->getFormatted('subdomain')); + } + + public function testEmailNormalization(): void + { + $email = new Email(' USER@EXAMPLE.COM '); + + $this->assertEquals('user@example.com', $email->get()); + $this->assertEquals('user@example.com', $email->normalize()); + } + + public function testInvalidEmailEmpty(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("Email address cannot be empty"); + + new Email(''); + } + + public function testInvalidEmailNoAt(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("'invalid-email' must be a valid email address"); + + new Email('invalid-email'); + } + + public function testInvalidEmailMultipleAt(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("'user@example@com' must be a valid email address"); + + new Email('user@example@com'); + } + + public function testInvalidEmailNoLocal(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("'@example.com' must be a valid email address"); + + new Email('@example.com'); + } + + public function testInvalidEmailNoDomain(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage("'user@' must be a valid email address"); + + new Email('user@'); + } + + public function testInvalidEmailConsecutiveDots(): void + { + $email = new Email('user..name@example.com'); + + $this->assertEquals(false, $email->hasValidLocal()); + } + + public function testInvalidEmailStartsWithDot(): void + { + $email = new Email('.user@example.com'); + + $this->assertEquals(false, $email->hasValidLocal()); + } + + public function testInvalidEmailEndsWithDot(): void + { + $email = new Email('user.@example.com'); + + $this->assertEquals(false, $email->hasValidLocal()); + } + + public function testInvalidEmailLocalTooLong(): void + { + $longLocal = str_repeat('a', 65); // 65 characters + $email = new Email($longLocal . '@example.com'); + + $this->assertEquals(false, $email->hasValidLocal()); + } + + public function testInvalidEmailDomainTooLong(): void + { + $longDomain = str_repeat('a', 250) . '.com'; // 254 characters + $email = new Email('user@' . $longDomain); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainConsecutiveDots(): void + { + $email = new Email('user@example..com'); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainConsecutiveHyphens(): void + { + $email = new Email('user@example--com.com'); + + // filter_var allows consecutive hyphens, so this will be valid + $this->assertEquals(true, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainStartsWithDot(): void + { + $email = new Email('user@.example.com'); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainEndsWithDot(): void + { + $email = new Email('user@example.com.'); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainStartsWithHyphen(): void + { + $email = new Email('user@-example.com'); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainEndsWithHyphen(): void + { + $email = new Email('user@example-.com'); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainNoTLD(): void + { + $email = new Email('user@example'); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailDomainInvalidCharacters(): void + { + $email = new Email('user@example!.com'); + + $this->assertEquals(false, $email->hasValidDomain()); + } + + public function testInvalidEmailLocalInvalidCharacters(): void + { + $email = new Email('user!@example.com'); + + $this->assertEquals(false, $email->hasValidLocal()); + } + + public function testFreeEmailProviders(): void + { + $freeProviders = [ + 'gmail.com', + 'yahoo.com', + 'hotmail.com', + 'outlook.com', + 'live.com', + 'aol.com', + 'icloud.com', + 'protonmail.com', + 'zoho.com', + 'yandex.com', + 'mail.com', + 'gmx.com', + 'web.de', + 'tutanota.com', + 'fastmail.com', + 'hey.com' + ]; + + foreach ($freeProviders as $provider) { + $email = new Email('user@' . $provider); + $this->assertEquals(true, $email->isFree(), "Failed for provider: {$provider}"); + $this->assertEquals(false, $email->isCorporate(), "Failed for provider: {$provider}"); + } + } + + public function testDisposableEmailProviders(): void + { + $disposableProviders = [ + '10minutemail.com', + 'tempmail.org', + 'guerrillamail.com', + 'mailinator.com', + 'yopmail.com', + 'temp-mail.org', + 'throwaway.email', + 'getnada.com', + 'maildrop.cc', + 'sharklasers.com', + 'test.com' + ]; + + foreach ($disposableProviders as $provider) { + $email = new Email('user@' . $provider); + $this->assertEquals(true, $email->isDisposable(), "Failed for provider: {$provider}"); + $this->assertEquals(false, $email->isCorporate(), "Failed for provider: {$provider}"); + } + } + + public function testCorporateEmailProviders(): void + { + $corporateProviders = [ + 'company.com', + 'business.org', + 'enterprise.net', + 'corporation.co.uk', + 'organization.org', + 'firm.com', + 'office.net', + 'work.org' + ]; + + foreach ($corporateProviders as $provider) { + $email = new Email('user@' . $provider); + $this->assertEquals(false, $email->isFree(), "Failed for provider: {$provider}"); + $this->assertEquals(false, $email->isDisposable(), "Failed for provider: {$provider}"); + $this->assertEquals(true, $email->isCorporate(), "Failed for provider: {$provider}"); + } + } +} diff --git a/tests/Validator/EmailCorporateTest.php b/tests/Validator/EmailCorporateTest.php new file mode 100644 index 0000000..b215912 --- /dev/null +++ b/tests/Validator/EmailCorporateTest.php @@ -0,0 +1,122 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Validator; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Validator\EmailCorporate; + +class EmailCorporateTest extends TestCase +{ + public function testValidCorporateEmail(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals(true, $validator->isValid('test@company.com')); + $this->assertEquals(true, $validator->isValid('user@business.org')); + $this->assertEquals(true, $validator->isValid('user@enterprise.net')); + $this->assertEquals(true, $validator->isValid('user@corporation.co.uk')); + $this->assertEquals(true, $validator->isValid('user@organization.org')); + $this->assertEquals(true, $validator->isValid('user@firm.com')); + $this->assertEquals(true, $validator->isValid('user@office.net')); + $this->assertEquals(true, $validator->isValid('user@work.org')); + } + + public function testInvalidFreeEmail(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals(false, $validator->isValid('user@gmail.com')); + $this->assertEquals(false, $validator->isValid('user@yahoo.com')); + $this->assertEquals(false, $validator->isValid('user@hotmail.com')); + $this->assertEquals(false, $validator->isValid('user@outlook.com')); + $this->assertEquals(false, $validator->isValid('user@live.com')); + $this->assertEquals(false, $validator->isValid('user@aol.com')); + $this->assertEquals(false, $validator->isValid('user@icloud.com')); + $this->assertEquals(false, $validator->isValid('user@protonmail.com')); + $this->assertEquals(false, $validator->isValid('user@zoho.com')); + $this->assertEquals(false, $validator->isValid('user@yandex.com')); + $this->assertEquals(false, $validator->isValid('user@mail.com')); + $this->assertEquals(false, $validator->isValid('user@gmx.com')); + $this->assertEquals(false, $validator->isValid('user@web.de')); + $this->assertEquals(false, $validator->isValid('user@tutanota.com')); + $this->assertEquals(false, $validator->isValid('user@fastmail.com')); + $this->assertEquals(false, $validator->isValid('user@hey.com')); + } + + public function testInvalidDisposableEmail(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals(false, $validator->isValid('user@10minutemail.com')); + $this->assertEquals(false, $validator->isValid('user@tempmail.org')); + $this->assertEquals(false, $validator->isValid('user@guerrillamail.com')); + $this->assertEquals(false, $validator->isValid('user@mailinator.com')); + $this->assertEquals(false, $validator->isValid('user@yopmail.com')); + $this->assertEquals(false, $validator->isValid('user@temp-mail.org')); + $this->assertEquals(false, $validator->isValid('user@throwaway.email')); + $this->assertEquals(false, $validator->isValid('user@getnada.com')); + $this->assertEquals(false, $validator->isValid('user@maildrop.cc')); + $this->assertEquals(false, $validator->isValid('user@sharklasers.com')); + $this->assertEquals(false, $validator->isValid('user@test.com')); + // example.com is no longer considered disposable, so it's corporate + $this->assertEquals(true, $validator->isValid('user@example.com')); + $this->assertEquals(true, $validator->isValid('user@example.org')); + $this->assertEquals(true, $validator->isValid('user@example.net')); + } + + public function testInvalidEmailFormat(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals(false, $validator->isValid('')); + $this->assertEquals(false, $validator->isValid('invalid-email')); + $this->assertEquals(false, $validator->isValid('user@example@com')); + $this->assertEquals(false, $validator->isValid('@example.com')); + $this->assertEquals(false, $validator->isValid('user@')); + } + + public function testNonStringInput(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals(false, $validator->isValid(null)); + $this->assertEquals(false, $validator->isValid(123)); + $this->assertEquals(false, $validator->isValid([])); + $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(true)); + $this->assertEquals(false, $validator->isValid(false)); + } + + public function testValidatorDescription(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals('Value must be a valid email address from a corporate domain', $validator->getDescription()); + } + + public function testValidatorType(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals('string', $validator->getType()); + } + + public function testValidatorIsArray(): void + { + $validator = new EmailCorporate(); + + $this->assertEquals(false, $validator->isArray()); + } +} diff --git a/tests/Validator/EmailDomainTest.php b/tests/Validator/EmailDomainTest.php new file mode 100644 index 0000000..6e5f7ee --- /dev/null +++ b/tests/Validator/EmailDomainTest.php @@ -0,0 +1,82 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Validator; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Validator\EmailDomain; + +class EmailDomainTest extends TestCase +{ + public function testValidEmailDomain(): void + { + $validator = new EmailDomain(); + + $this->assertEquals(true, $validator->isValid('test@example.com')); + $this->assertEquals(true, $validator->isValid('user@mail.example.com')); + $this->assertEquals(true, $validator->isValid('user@mail.sub.example.com')); + $this->assertEquals(true, $validator->isValid('user@example-domain.com')); + $this->assertEquals(true, $validator->isValid('user@example123.com')); + } + + public function testInvalidEmailDomain(): void + { + $validator = new EmailDomain(); + + $this->assertEquals(false, $validator->isValid('')); + $this->assertEquals(false, $validator->isValid('invalid-email')); + $this->assertEquals(false, $validator->isValid('user@example..com')); + // filter_var allows consecutive hyphens, so this will be valid + $this->assertEquals(true, $validator->isValid('user@example--com.com')); + $this->assertEquals(false, $validator->isValid('user@.example.com')); + $this->assertEquals(false, $validator->isValid('user@example.com.')); + $this->assertEquals(false, $validator->isValid('user@-example.com')); + $this->assertEquals(false, $validator->isValid('user@example-.com')); + $this->assertEquals(false, $validator->isValid('user@example')); + $this->assertEquals(false, $validator->isValid('user@example!.com')); + } + + public function testNonStringInput(): void + { + $validator = new EmailDomain(); + + $this->assertEquals(false, $validator->isValid(null)); + $this->assertEquals(false, $validator->isValid(123)); + $this->assertEquals(false, $validator->isValid([])); + $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(true)); + $this->assertEquals(false, $validator->isValid(false)); + } + + public function testValidatorDescription(): void + { + $validator = new EmailDomain(); + + $this->assertEquals('Value must be a valid email address with a valid domain', $validator->getDescription()); + } + + public function testValidatorType(): void + { + $validator = new EmailDomain(); + + $this->assertEquals('string', $validator->getType()); + } + + public function testValidatorIsArray(): void + { + $validator = new EmailDomain(); + + $this->assertEquals(false, $validator->isArray()); + } +} diff --git a/tests/Validator/EmailLocalTest.php b/tests/Validator/EmailLocalTest.php new file mode 100644 index 0000000..2da3dca --- /dev/null +++ b/tests/Validator/EmailLocalTest.php @@ -0,0 +1,79 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Validator; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Validator\EmailLocal; + +class EmailLocalTest extends TestCase +{ + public function testValidEmailLocal(): void + { + $validator = new EmailLocal(); + + $this->assertEquals(true, $validator->isValid('test@example.com')); + $this->assertEquals(true, $validator->isValid('user.name+tag@example.com')); + $this->assertEquals(true, $validator->isValid('user-name@example.com')); + $this->assertEquals(true, $validator->isValid('user_name@example.com')); + $this->assertEquals(true, $validator->isValid('user123@example.com')); + $this->assertEquals(true, $validator->isValid('user.name.last@example.com')); + } + + public function testInvalidEmailLocal(): void + { + $validator = new EmailLocal(); + + $this->assertEquals(false, $validator->isValid('')); + $this->assertEquals(false, $validator->isValid('invalid-email')); + $this->assertEquals(false, $validator->isValid('user..name@example.com')); + $this->assertEquals(false, $validator->isValid('.user@example.com')); + $this->assertEquals(false, $validator->isValid('user.@example.com')); + $this->assertEquals(false, $validator->isValid('user!@example.com')); + } + + public function testNonStringInput(): void + { + $validator = new EmailLocal(); + + $this->assertEquals(false, $validator->isValid(null)); + $this->assertEquals(false, $validator->isValid(123)); + $this->assertEquals(false, $validator->isValid([])); + $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(true)); + $this->assertEquals(false, $validator->isValid(false)); + } + + public function testValidatorDescription(): void + { + $validator = new EmailLocal(); + + $this->assertEquals('Value must be a valid email address with a valid local part', $validator->getDescription()); + } + + public function testValidatorType(): void + { + $validator = new EmailLocal(); + + $this->assertEquals('string', $validator->getType()); + } + + public function testValidatorIsArray(): void + { + $validator = new EmailLocal(); + + $this->assertEquals(false, $validator->isArray()); + } +} + diff --git a/tests/Validator/EmailNotDisposableTest.php b/tests/Validator/EmailNotDisposableTest.php new file mode 100644 index 0000000..6ff402d --- /dev/null +++ b/tests/Validator/EmailNotDisposableTest.php @@ -0,0 +1,97 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Validator; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Validator\EmailNotDisposable; + +class EmailNotDisposableTest extends TestCase +{ + public function testValidNonDisposableEmail(): void + { + $validator = new EmailNotDisposable(); + + $this->assertEquals(true, $validator->isValid('test@example.com')); + $this->assertEquals(true, $validator->isValid('user@gmail.com')); + $this->assertEquals(true, $validator->isValid('user@yahoo.com')); + $this->assertEquals(true, $validator->isValid('user@company.com')); + $this->assertEquals(true, $validator->isValid('user@business.org')); + } + + public function testInvalidDisposableEmail(): void + { + $validator = new EmailNotDisposable(); + + $this->assertEquals(false, $validator->isValid('user@10minutemail.com')); + $this->assertEquals(false, $validator->isValid('user@tempmail.org')); + $this->assertEquals(false, $validator->isValid('user@guerrillamail.com')); + $this->assertEquals(false, $validator->isValid('user@mailinator.com')); + $this->assertEquals(false, $validator->isValid('user@yopmail.com')); + $this->assertEquals(false, $validator->isValid('user@temp-mail.org')); + $this->assertEquals(false, $validator->isValid('user@throwaway.email')); + $this->assertEquals(false, $validator->isValid('user@getnada.com')); + $this->assertEquals(false, $validator->isValid('user@maildrop.cc')); + $this->assertEquals(false, $validator->isValid('user@sharklasers.com')); + $this->assertEquals(false, $validator->isValid('user@test.com')); + // example.com is no longer considered disposable + $this->assertEquals(true, $validator->isValid('user@example.com')); + $this->assertEquals(true, $validator->isValid('user@example.org')); + $this->assertEquals(true, $validator->isValid('user@example.net')); + } + + public function testInvalidEmailFormat(): void + { + $validator = new EmailNotDisposable(); + + $this->assertEquals(false, $validator->isValid('')); + $this->assertEquals(false, $validator->isValid('invalid-email')); + $this->assertEquals(false, $validator->isValid('user@example@com')); + $this->assertEquals(false, $validator->isValid('@example.com')); + $this->assertEquals(false, $validator->isValid('user@')); + } + + public function testNonStringInput(): void + { + $validator = new EmailNotDisposable(); + + $this->assertEquals(false, $validator->isValid(null)); + $this->assertEquals(false, $validator->isValid(123)); + $this->assertEquals(false, $validator->isValid([])); + $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(true)); + $this->assertEquals(false, $validator->isValid(false)); + } + + public function testValidatorDescription(): void + { + $validator = new EmailNotDisposable(); + + $this->assertEquals('Value must be a valid email address that is not from a disposable email service', $validator->getDescription()); + } + + public function testValidatorType(): void + { + $validator = new EmailNotDisposable(); + + $this->assertEquals('string', $validator->getType()); + } + + public function testValidatorIsArray(): void + { + $validator = new EmailNotDisposable(); + + $this->assertEquals(false, $validator->isArray()); + } +} diff --git a/tests/Validator/EmailTest.php b/tests/Validator/EmailTest.php new file mode 100644 index 0000000..f9b2de4 --- /dev/null +++ b/tests/Validator/EmailTest.php @@ -0,0 +1,93 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Validator; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Validator\Email; + +class EmailTest extends TestCase +{ + public function testValidEmail(): void + { + $validator = new Email(); + + $this->assertEquals(true, $validator->isValid('test@example.com')); + $this->assertEquals(true, $validator->isValid('user.name+tag@example.com')); + $this->assertEquals(true, $validator->isValid('user-name@example-domain.com')); + $this->assertEquals(true, $validator->isValid('user_name@example.com')); + $this->assertEquals(true, $validator->isValid('user123@example123.com')); + $this->assertEquals(true, $validator->isValid('user.name.last@example.com')); + $this->assertEquals(true, $validator->isValid('user@mail.example.com')); + $this->assertEquals(true, $validator->isValid('user@mail.sub.example.com')); + } + + public function testInvalidEmail(): void + { + $validator = new Email(); + + $this->assertEquals(false, $validator->isValid('')); + $this->assertEquals(false, $validator->isValid('invalid-email')); + $this->assertEquals(false, $validator->isValid('user@example@com')); + $this->assertEquals(false, $validator->isValid('@example.com')); + $this->assertEquals(false, $validator->isValid('user@')); + $this->assertEquals(false, $validator->isValid('user..name@example.com')); + $this->assertEquals(false, $validator->isValid('.user@example.com')); + $this->assertEquals(false, $validator->isValid('user.@example.com')); + $this->assertEquals(false, $validator->isValid('user@example..com')); + // filter_var allows consecutive hyphens, so this will be valid + $this->assertEquals(true, $validator->isValid('user@example--com.com')); + $this->assertEquals(false, $validator->isValid('user@.example.com')); + $this->assertEquals(false, $validator->isValid('user@example.com.')); + $this->assertEquals(false, $validator->isValid('user@-example.com')); + $this->assertEquals(false, $validator->isValid('user@example-.com')); + $this->assertEquals(false, $validator->isValid('user@example')); + $this->assertEquals(false, $validator->isValid('user@example!.com')); + // filter_var allows exclamation marks in local part, so this will be valid + $this->assertEquals(true, $validator->isValid('user!@example.com')); + } + + public function testNonStringInput(): void + { + $validator = new Email(); + + $this->assertEquals(false, $validator->isValid(null)); + $this->assertEquals(false, $validator->isValid(123)); + $this->assertEquals(false, $validator->isValid([])); + $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(true)); + $this->assertEquals(false, $validator->isValid(false)); + } + + public function testValidatorDescription(): void + { + $validator = new Email(); + + $this->assertEquals('Value must be a valid email address', $validator->getDescription()); + } + + public function testValidatorType(): void + { + $validator = new Email(); + + $this->assertEquals('string', $validator->getType()); + } + + public function testValidatorIsArray(): void + { + $validator = new Email(); + + $this->assertEquals(false, $validator->isArray()); + } +} From 62b4fa8e0085d9aedc96930e772211f900bb0c3c Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 18 Oct 2025 22:40:21 +0100 Subject: [PATCH 02/21] Add Kikobeats as a new source for free email domains and update domain lists --- data/disposable-domains.php | 3 +- data/free-domains-manual.php | 2 + data/free-domains.php | 4782 +++++++++++++++++++++++++++++++++- data/sources.php | 6 + import.php | 64 + 5 files changed, 4853 insertions(+), 4 deletions(-) diff --git a/data/disposable-domains.php b/data/disposable-domains.php index c07e96f..e328d75 100644 --- a/data/disposable-domains.php +++ b/data/disposable-domains.php @@ -4,7 +4,7 @@ * Disposable Email Domains * * This file contains a list of known Disposable Email Domains. - * Last updated: 2025-10-18 21:32:46 + * Last updated: 2025-10-18 21:39:42 * * Format: Indexed array of domain names */ @@ -21214,7 +21214,6 @@ 'elcajonrentals.com', 'elcarin.store', 'elchato.com', - 'eldad.com', 'elderflame.xyz', 'eldobhato-level.hu', 'eldoradoschool.org', diff --git a/data/free-domains-manual.php b/data/free-domains-manual.php index c1ecf83..b747708 100644 --- a/data/free-domains-manual.php +++ b/data/free-domains-manual.php @@ -11,4 +11,6 @@ */ return [ + 'walla.co.il', + 'walla.com', ]; diff --git a/data/free-domains.php b/data/free-domains.php index 264ce7e..c82dc3d 100644 --- a/data/free-domains.php +++ b/data/free-domains.php @@ -4,11 +4,4789 @@ * Free Email Domains * * This file contains a list of known Free Email Domains. - * Last updated: 2025-10-18 21:31:36 + * Last updated: 2025-10-18 21:39:43 * * Format: Indexed array of domain names */ return [ - 'eldad.com', + '0-mail.com', + '027168.com', + '0815.su', + '0sg.net', + '10mail.org', + '10minutemail.co.za', + '10minutemail.com', + '11mail.com', + '123.com', + '123box.net', + '123india.com', + '123mail.cl', + '123mail.org', + '123qwe.co.uk', + '126.com', + '139.com', + '150mail.com', + '150ml.com', + '15meg4free.com', + '163.com', + '16mail.com', + '188.com', + '189.cn', + '1ce.us', + '1chuan.com', + '1coolplace.com', + '1freeemail.com', + '1funplace.com', + '1internetdrive.com', + '1mail.ml', + '1mail.net', + '1me.net', + '1mum.com', + '1musicrow.com', + '1netdrive.com', + '1nsyncfan.com', + '1pad.de', + '1under.com', + '1webave.com', + '1webhighway.com', + '1zhuan.com', + '2-mail.com', + '20email.eu', + '20mail.in', + '20mail.it', + '212.com', + '21cn.com', + '24horas.com', + '2911.net', + '2980.com', + '2bmail.co.uk', + '2d2i.com', + '2die4.com', + '2trom.com', + '3000.it', + '30minutesmail.com', + '3126.com', + '321media.com', + '33mail.com', + '37.com', + '3ammagazine.com', + '3dmail.com', + '3email.com', + '3g.ua', + '3mail.ga', + '3xl.net', + '444.net', + '4email.com', + '4email.net', + '4mg.com', + '4newyork.com', + '4warding.net', + '4warding.org', + '4x4man.com', + '50mail.com', + '60minutemail.com', + '6ip.us', + '6mail.cf', + '6paq.com', + '74.ru', + '74gmail.com', + '7mail.ga', + '7mail.ml', + '88.am', + '8848.net', + '8mail.ga', + '8mail.ml', + '97rock.com', + '99experts.com', + 'a45.in', + 'aaamail.zzn.com', + 'aamail.net', + 'aapt.net.au', + 'aaronkwok.net', + 'abbeyroadlondon.co.uk', + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com', + 'abcflash.net', + 'abdulnour.com', + 'aberystwyth.com', + 'about.com', + 'abusemail.de', + 'abv.bg', + 'abwesend.de', + 'abyssmail.com', + 'ac20mail.in', + 'academycougars.com', + 'acceso.or.cr', + 'access4less.net', + 'accessgcc.com', + 'accountant.com', + 'acdcfan.com', + 'ace-of-base.com', + 'acmemail.net', + 'acninc.net', + 'activist.com', + 'adam.com.au', + 'add3000.pp.ua', + 'addcom.de', + 'address.com', + 'adelphia.net', + 'adexec.com', + 'adfarrow.com', + 'adios.net', + 'adoption.com', + 'ados.fr', + 'adrenalinefreak.com', + 'advalvas.be', + 'advantimo.com', + 'aeiou.pt', + 'aemail4u.com', + 'aeneasmail.com', + 'afreeinternet.com', + 'africamail.com', + 'africamel.net', + 'ag.us.to', + 'agoodmail.com', + 'ahaa.dk', + 'ahk.jp', + 'aichi.com', + 'aim.com', + 'aircraftmail.com', + 'airforce.net', + 'airforceemail.com', + 'airpost.net', + 'ajacied.com', + 'ajaxapp.net', + 'ak47.hu', + 'aknet.kg', + 'albawaba.com', + 'alex4all.com', + 'alexandria.cc', + 'algeria.com', + 'alhilal.net', + 'alibaba.com', + 'alice.it', + 'alive.cz', + 'aliyun.com', + 'allergist.com', + 'allmail.net', + 'alloymail.com', + 'allracing.com', + 'allsaintsfan.com', + 'alpenjodel.de', + 'alphafrau.de', + 'alskens.dk', + 'altavista.com', + 'altavista.net', + 'altavista.se', + 'alternativagratis.com', + 'alumni.com', + 'alumnidirector.com', + 'alvilag.hu', + 'amail.com', + 'amazonses.com', + 'amele.com', + 'america.hm', + 'ameritech.net', + 'amnetsal.com', + 'amorki.pl', + 'amrer.net', + 'amuro.net', + 'amuromail.com', + 'ananzi.co.za', + 'andylau.net', + 'anfmail.com', + 'angelfire.com', + 'angelic.com', + 'animail.net', + 'animalhouse.com', + 'animalwoman.net', + 'anjungcafe.com', + 'annsmail.com', + 'ano-mail.net', + 'anonmails.de', + 'anonymous.to', + 'anote.com', + 'another.com', + 'anotherdomaincyka.tk', + 'anotherwin95.com', + 'anti-social.com', + 'antisocial.com', + 'antispam24.de', + 'antongijsen.com', + 'antwerpen.com', + 'anymoment.com', + 'anytimenow.com', + 'aol.com', + 'aon.at', + 'apexmail.com', + 'apmail.com', + 'apollo.lv', + 'aport.ru', + 'aport2000.ru', + 'appraiser.net', + 'approvers.net', + 'arabia.com', + 'arabtop.net', + 'archaeologist.com', + 'arcor.de', + 'arcotronics.bg', + 'arcticmail.com', + 'argentina.com', + 'aristotle.org', + 'army.net', + 'armyspy.com', + 'arnet.com.ar', + 'art-en-ligne.pro', + 'artlover.com', + 'artlover.com.au', + 'as-if.com', + 'asdasd.nl', + 'asean-mail.com', + 'asheville.com', + 'asia-links.com', + 'asia-mail.com', + 'asiafind.com', + 'asianavenue.com', + 'asiancityweb.com', + 'asiansonly.net', + 'asianwired.net', + 'asiapoint.net', + 'ass.pp.ua', + 'assala.com', + 'assamesemail.com', + 'astroboymail.com', + 'astrolover.com', + 'astrosfan.com', + 'astrosfan.net', + 'asurfer.com', + 'atheist.com', + 'athenachu.net', + 'atina.cl', + 'atl.lv', + 'atlaswebmail.com', + 'atmc.net', + 'atozasia.com', + 'atrus.ru', + 'att.net', + 'attglobal.net', + 'attymail.com', + 'au.ru', + 'auctioneer.net', + 'ausi.com', + 'aussiemail.com.au', + 'austin.rr.com', + 'australia.edu', + 'australiamail.com', + 'austrosearch.net', + 'autoescuelanerja.com', + 'autograf.pl', + 'autorambler.ru', + 'avh.hu', + 'avia-tonic.fr', + 'awsom.net', + 'axoskate.com', + 'ayna.com', + 'azazazatashkent.tk', + 'azimiweb.com', + 'azmeil.tk', + 'bachelorboy.com', + 'bachelorgal.com', + 'backpackers.com', + 'backstreet-boys.com', + 'backstreetboysclub.com', + 'bagherpour.com', + 'baldmama.de', + 'baldpapa.de', + 'ballyfinance.com', + 'bangkok.com', + 'bangkok2000.com', + 'bannertown.net', + 'baptistmail.com', + 'baptized.com', + 'barcelona.com', + 'bareed.ws', + 'bartender.net', + 'baseballmail.com', + 'basketballmail.com', + 'batuta.net', + 'baudoinconsulting.com', + 'bboy.zzn.com', + 'bcvibes.com', + 'beddly.com', + 'beeebank.com', + 'beenhad.com', + 'beep.ru', + 'beer.com', + 'beethoven.com', + 'belice.com', + 'belizehome.com', + 'bell.net', + 'bellair.net', + 'bellsouth.net', + 'berlin.com', + 'berlin.de', + 'berlinexpo.de', + 'bestmail.us', + 'betriebsdirektor.de', + 'bettergolf.net', + 'bharatmail.com', + 'big1.us', + 'bigassweb.com', + 'bigblue.net.au', + 'bigboab.com', + 'bigfoot.com', + 'bigfoot.de', + 'bigger.com', + 'biggerbadder.com', + 'bigmailbox.com', + 'bigmir.net', + 'bigpond.au', + 'bigpond.com', + 'bigpond.com.au', + 'bigpond.net', + 'bigpond.net.au', + 'bigramp.com', + 'bigstring.com', + 'bikemechanics.com', + 'bikeracer.com', + 'bikeracers.net', + 'bikerider.com', + 'billsfan.com', + 'billsfan.net', + 'bimla.net', + 'bin-wieder-da.de', + 'bio-muesli.info', + 'birdlover.com', + 'birdowner.net', + 'bisons.com', + 'bitmail.com', + 'bitpage.net', + 'bizhosting.com', + 'bk.ru', + 'blackburnmail.com', + 'blackplanet.com', + 'blader.com', + 'bladesmail.net', + 'blazemail.com', + 'bleib-bei-mir.de', + 'blockfilter.com', + 'blogmyway.org', + 'bluebottle.com', + 'bluehyppo.com', + 'bluemail.ch', + 'bluemail.dk', + 'bluesfan.com', + 'bluewin.ch', + 'blueyonder.co.uk', + 'blushmail.com', + 'blutig.me', + 'bmlsports.net', + 'boardermail.com', + 'boatracers.com', + 'bodhi.lawlita.com', + 'bol.com.br', + 'bolando.com', + 'bollywoodz.com', + 'boltonfans.com', + 'bombdiggity.com', + 'bonbon.net', + 'boom.com', + 'bootmail.com', + 'bootybay.de', + 'bornnaked.com', + 'bostonoffice.com', + 'boun.cr', + 'bounce.net', + 'bounces.amazon.com', + 'bouncr.com', + 'box.az', + 'box.ua', + 'boxbg.com', + 'boxemail.com', + 'boxformail.in', + 'boxfrog.com', + 'boximail.com', + 'boyzoneclub.com', + 'bradfordfans.com', + 'brasilia.net', + 'brazilmail.com', + 'brazilmail.com.br', + 'breadtimes.press', + 'breathe.com', + 'brennendesreich.de', + 'bresnan.net', + 'brew-master.com', + 'brew-meister.com', + 'brfree.com.br', + 'briefemail.com', + 'bright.net', + 'britneyclub.com', + 'brittonsign.com', + 'broadcast.net', + 'brokenvalve.com', + 'brusseler.com', + 'bsdmail.com', + 'btcmail.pw', + 'btconnect.co.uk', + 'btconnect.com', + 'btinternet.com', + 'btopenworld.co.uk', + 'buerotiger.de', + 'buffymail.com', + 'bullsfan.com', + 'bullsgame.com', + 'bumerang.ro', + 'bumpymail.com', + 'bund.us', + 'burnthespam.info', + 'burstmail.info', + 'buryfans.com', + 'business-man.com', + 'businessman.net', + 'busta-rhymes.com', + 'buyersusa.com', + 'bvimailbox.com', + 'byom.de', + 'c2.hu', + 'c2i.net', + 'c3.hu', + 'c4.com', + 'c51vsgq.com', + 'cabacabana.com', + 'cable.comcast.com', + 'cableone.net', + 'caere.it', + 'cairomail.com', + 'calendar-server.bounces.google.com', + 'calidifontain.be', + 'californiamail.com', + 'callnetuk.com', + 'callsign.net', + 'caltanet.it', + 'camidge.com', + 'canada-11.com', + 'canada.com', + 'canadianmail.com', + 'canoemail.com', + 'canwetalk.com', + 'caramail.com', + 'care2.com', + 'careerbuildermail.com', + 'carioca.net', + 'cartelera.org', + 'cartestraina.ro', + 'casablancaresort.com', + 'casema.nl', + 'cash4u.com', + 'cashette.com', + 'casino.com', + 'catcha.com', + 'catchamail.com', + 'catholic.org', + 'catlover.com', + 'cd2.com', + 'celineclub.com', + 'celtic.com', + 'center-mail.de', + 'centermail.at', + 'centermail.de', + 'centermail.info', + 'centoper.it', + 'centralpets.com', + 'centrum.cz', + 'centrum.sk', + 'centurytel.net', + 'certifiedmail.com', + 'cfl.rr.com', + 'cgac.es', + 'cghost.s-a-d.de', + 'chacuo.net', + 'chaiyomail.com', + 'chammy.info', + 'chance2mail.com', + 'chandrasekar.net', + 'charmedmail.com', + 'charter.net', + 'chat.ru', + 'chattown.com', + 'chauhanweb.com', + 'cheatmail.de', + 'chechnya.conf.work', + 'check.com', + 'check1check.com', + 'cheerful.com', + 'chef.net', + 'chek.com', + 'chello.nl', + 'chemist.com', + 'chequemail.com', + 'cheyenneweb.com', + 'chez.com', + 'chickmail.com', + 'china.com', + 'china.net.vg', + 'chinamail.com', + 'chirk.com', + 'chocaholic.com.au', + 'chong-mail.com', + 'chong-mail.net', + 'churchusa.com', + 'cia-agent.com', + 'cia.hu', + 'ciaoweb.it', + 'cicciociccio.com', + 'cinci.rr.com', + 'cincinow.net', + 'citiz.net', + 'citlink.net', + 'citromail.hu', + 'city-of-bath.org', + 'city-of-birmingham.com', + 'city-of-brighton.org', + 'city-of-cambridge.com', + 'city-of-coventry.com', + 'city-of-edinburgh.com', + 'city-of-lichfield.com', + 'city-of-lincoln.com', + 'city-of-liverpool.com', + 'city-of-manchester.com', + 'city-of-nottingham.com', + 'city-of-oxford.com', + 'city-of-swansea.com', + 'city-of-westminster.com', + 'city-of-westminster.net', + 'city-of-york.net', + 'cityofcardiff.net', + 'cityoflondon.org', + 'ckaazaza.tk', + 'claramail.com', + 'classicalfan.com', + 'classicmail.co.za', + 'clear.net.nz', + 'clearwire.net', + 'clerk.com', + 'cliffhanger.com', + 'clixser.com', + 'close2you.net', + 'clrmail.com', + 'club4x4.net', + 'clubalfa.com', + 'clubbers.net', + 'clubducati.com', + 'clubhonda.net', + 'clubmember.org', + 'clubnetnoir.com', + 'clubvdo.net', + 'cluemail.com', + 'cmail.net', + 'cmpmail.com', + 'cnnsimail.com', + 'cntv.cn', + 'codec.ro', + 'coder.hu', + 'coid.biz', + 'coldmail.com', + 'collectiblesuperstore.com', + 'collector.org', + 'collegeclub.com', + 'collegemail.com', + 'colleges.com', + 'columbus.rr.com', + 'columbusrr.com', + 'columnist.com', + 'comcast.net', + 'comic.com', + 'communityconnect.com', + 'comporium.net', + 'comprendemail.com', + 'compuserve.com', + 'computer-freak.com', + 'computer4u.com', + 'computermail.net', + 'conexcol.com', + 'conk.com', + 'connect4free.net', + 'connectbox.com', + 'consultant.com', + 'consumerriot.com', + 'contractor.net', + 'contrasto.cu.cc', + 'cookiemonster.com', + 'cool.br', + 'coole-files.de', + 'coolgoose.ca', + 'coolgoose.com', + 'coolkiwi.com', + 'coollist.com', + 'coolmail.com', + 'coolmail.net', + 'coolsend.com', + 'coolsite.net', + 'cooooool.com', + 'cooperation.net', + 'cooperationtogo.net', + 'copacabana.com', + 'copper.net', + 'cornells.com', + 'cornerpub.com', + 'corporatedirtbag.com', + 'correo.terra.com.gt', + 'cortinet.com', + 'cotas.net', + 'counsellor.com', + 'countrylover.com', + 'cox.com', + 'cox.net', + 'coxinet.net', + 'cracker.hu', + 'crapmail.org', + 'crazedanddazed.com', + 'crazymailing.com', + 'crazysexycool.com', + 'cristianemail.com', + 'critterpost.com', + 'croeso.com', + 'crosshairs.com', + 'crosswinds.net', + 'crwmail.com', + 'cry4helponline.com', + 'cs.com', + 'csinibaba.hu', + 'cuemail.com', + 'curio-city.com', + 'curryworld.de', + 'cute-girl.com', + 'cuteandcuddly.com', + 'cutey.com', + 'cww.de', + 'cyber-africa.net', + 'cyber-innovation.club', + 'cyber-matrix.com', + 'cyber-phone.eu', + 'cyber-wizard.com', + 'cyber4all.com', + 'cyberbabies.com', + 'cybercafemaui.com', + 'cyberdude.com', + 'cyberforeplay.net', + 'cybergal.com', + 'cybergrrl.com', + 'cyberinbox.com', + 'cyberleports.com', + 'cybermail.net', + 'cybernet.it', + 'cyberservices.com', + 'cyberspace-asia.com', + 'cybertrains.org', + 'cyclefanz.com', + 'cynetcity.com', + 'dabsol.net', + 'dadacasa.com', + 'daha.com', + 'dailypioneer.com', + 'dallasmail.com', + 'dangerous-minds.com', + 'dansegulvet.com', + 'dasdasdascyka.tk', + 'data54.com', + 'davegracey.com', + 'dawnsonmail.com', + 'dawsonmail.com', + 'dazedandconfused.com', + 'dbzmail.com', + 'dcemail.com', + 'deadlymob.org', + 'deagot.com', + 'deal-maker.com', + 'dearriba.com', + 'death-star.com', + 'deliveryman.com', + 'deneg.net', + 'depechemode.com', + 'deseretmail.com', + 'desertmail.com', + 'desilota.com', + 'deskpilot.com', + 'destin.com', + 'detik.com', + 'deutschland-net.com', + 'devotedcouples.com', + 'dezigner.ru', + 'dfwatson.com', + 'di-ve.com', + 'die-besten-bilder.de', + 'die-genossen.de', + 'die-optimisten.de', + 'die-optimisten.net', + 'diemailbox.de', + 'digibel.be', + 'digital-filestore.de', + 'diplomats.com', + 'directbox.com', + 'dirtracer.com', + 'discard.email', + 'discard.ga', + 'discard.gq', + 'disciples.com', + 'discofan.com', + 'discoverymail.com', + 'disign-concept.eu', + 'disign-revelation.com', + 'disinfo.net', + 'dispomail.eu', + 'disposable.com', + 'dispose.it', + 'dm.w3internet.co.uk', + 'dmailman.com', + 'dnainternet.net', + 'dnsmadeeasy.com', + 'doclist.bounces.google.com', + 'docmail.cz', + 'docs.google.com', + 'doctor.com', + 'dodgit.org', + 'dodo.com.au', + 'dodsi.com', + 'dog.com', + 'dogit.com', + 'doglover.com', + 'dogmail.co.uk', + 'dogsnob.net', + 'doityourself.com', + 'domforfb1.tk', + 'domforfb2.tk', + 'domforfb3.tk', + 'domforfb4.tk', + 'domforfb5.tk', + 'domforfb6.tk', + 'domforfb7.tk', + 'domforfb8.tk', + 'domozmail.com', + 'doneasy.com', + 'donjuan.com', + 'dontgotmail.com', + 'dontmesswithtexas.com', + 'doramail.com', + 'dostmail.com', + 'dotcom.fr', + 'dotmsg.com', + 'dott.it', + 'download-privat.de', + 'dplanet.ch', + 'dr.com', + 'dragoncon.net', + 'dropmail.me', + 'dropzone.com', + 'drotposta.hu', + 'dubaimail.com', + 'dublin.com', + 'dublin.ie', + 'duck.com', + 'dumpmail.com', + 'dumpmail.de', + 'dumpyemail.com', + 'dunlopdriver.com', + 'dunloprider.com', + 'duno.com', + 'duskmail.com', + 'dutchmail.com', + 'dwp.net', + 'dygo.com', + 'dynamitemail.com', + 'e-apollo.lv', + 'e-mail.com.tr', + 'e-mail.dk', + 'e-mail.ru', + 'e-mail.ua', + 'e-mailanywhere.com', + 'e-mails.ru', + 'e-tapaal.com', + 'earthalliance.com', + 'earthcam.net', + 'earthdome.com', + 'earthling.net', + 'earthlink.net', + 'earthonline.net', + 'eastcoast.co.za', + 'eastmail.com', + 'easy.to', + 'easypost.com', + 'easytrashmail.com', + 'ec.rr.com', + 'ecardmail.com', + 'ecbsolutions.net', + 'echina.com', + 'ecolo-online.fr', + 'ecompare.com', + 'edmail.com', + 'ednatx.com', + 'edtnmail.com', + 'educacao.te.pt', + 'eelmail.com', + 'ehmail.com', + 'einrot.com', + 'einrot.de', + 'eintagsmail.de', + 'eircom.net', + 'elisanet.fi', + 'elitemail.org', + 'elsitio.com', + 'elvis.com', + 'elvisfan.com', + 'email-fake.gq', + 'email-london.co.uk', + 'email.biz', + 'email.cbes.net', + 'email.com', + 'email.cz', + 'email.ee', + 'email.it', + 'email.nu', + 'email.org', + 'email.ro', + 'email.ru', + 'email.si', + 'email.su', + 'email.ua', + 'email2me.net', + 'email4u.info', + 'emailacc.com', + 'emailaccount.com', + 'emailage.ga', + 'emailage.gq', + 'emailasso.net', + 'emailchoice.com', + 'emailcorner.net', + 'emailem.com', + 'emailengine.net', + 'emailengine.org', + 'emailer.hubspot.com', + 'emailforyou.net', + 'emailgo.de', + 'emailgroups.net', + 'emailinfive.com', + 'emailit.com', + 'emailondeck.com', + 'emailpinoy.com', + 'emailplanet.com', + 'emailplus.org', + 'emailproxsy.com', + 'emails.ga', + 'emails.incisivemedia.com', + 'emails.ru', + 'emailthe.net', + 'emailto.de', + 'emailuser.net', + 'emailx.net', + 'emailz.ga', + 'emailz.gq', + 'ematic.com', + 'embarqmail.com', + 'emeil.in', + 'emeil.ir', + 'emil.com', + 'eml.cc', + 'eml.pp.ua', + 'end-war.com', + 'enel.net', + 'engineer.com', + 'england.com', + 'england.edu', + 'englandmail.com', + 'epage.ru', + 'epatra.com', + 'ephemail.net', + 'epix.net', + 'epost.de', + 'eposta.hu', + 'eqqu.com', + 'eramail.co.za', + 'eresmas.com', + 'eriga.lv', + 'estranet.it', + 'ethos.st', + 'etoast.com', + 'etrademail.com', + 'etranquil.com', + 'etranquil.net', + 'eudoramail.com', + 'europamel.net', + 'europe.com', + 'europemail.com', + 'euroseek.com', + 'eurosport.com', + 'every1.net', + 'everyday.com.kh', + 'everymail.net', + 'everyone.net', + 'everytg.ml', + 'examnotes.net', + 'excite.co.jp', + 'excite.com', + 'excite.it', + 'execs.com', + 'exemail.com.au', + 'exg6.exghost.com', + 'existiert.net', + 'expressasia.com', + 'extenda.net', + 'extended.com', + 'eyepaste.com', + 'eyou.com', + 'ezcybersearch.com', + 'ezmail.egine.com', + 'ezmail.ru', + 'ezrs.com', + 'f-m.fm', + 'f1fans.net', + 'facebook-email.ga', + 'facebook.com', + 'facebookmail.com', + 'facebookmail.gq', + 'fahr-zur-hoelle.org', + 'fake-email.pp.ua', + 'fake-mail.cf', + 'fake-mail.ga', + 'fake-mail.ml', + 'fakemailz.com', + 'falseaddress.com', + 'fan.com', + 'fansonlymail.com', + 'fansworldwide.de', + 'fantasticmail.com', + 'farang.net', + 'farifluset.mailexpire.com', + 'faroweb.com', + 'fast-email.com', + 'fast-mail.fr', + 'fast-mail.org', + 'fastacura.com', + 'fastchevy.com', + 'fastchrysler.com', + 'fastem.com', + 'fastemail.us', + 'fastemailer.com', + 'fastermail.com', + 'fastest.cc', + 'fastimap.com', + 'fastkawasaki.com', + 'fastmail.ca', + 'fastmail.cn', + 'fastmail.co.uk', + 'fastmail.com', + 'fastmail.com.au', + 'fastmail.es', + 'fastmail.fm', + 'fastmail.im', + 'fastmail.in', + 'fastmail.jp', + 'fastmail.mx', + 'fastmail.net', + 'fastmail.nl', + 'fastmail.se', + 'fastmail.to', + 'fastmail.tw', + 'fastmail.us', + 'fastmailbox.net', + 'fastmazda.com', + 'fastmessaging.com', + 'fastmitsubishi.com', + 'fastnissan.com', + 'fastservice.com', + 'fastsubaru.com', + 'fastsuzuki.com', + 'fasttoyota.com', + 'fastyamaha.com', + 'fatcock.net', + 'fatflap.com', + 'fathersrightsne.org', + 'fax.ru', + 'fbi-agent.com', + 'fbi.hu', + 'fdfdsfds.com', + 'fea.st', + 'federalcontractors.com', + 'feinripptraeger.de', + 'felicitymail.com', + 'femenino.com', + 'fetchmail.co.uk', + 'fettabernett.de', + 'feyenoorder.com', + 'ffanet.com', + 'fiberia.com', + 'ficken.de', + 'fightallspam.com', + 'filipinolinks.com', + 'financemail.net', + 'financier.com', + 'findmail.com', + 'finebody.com', + 'fire-brigade.com', + 'fireman.net', + 'fishburne.org', + 'fishfuse.com', + 'fixmail.tk', + 'fizmail.com', + 'flashbox.5july.org', + 'flashmail.com', + 'flashmail.net', + 'fleckens.hu', + 'flipcode.com', + 'fmail.co.uk', + 'fmailbox.com', + 'fmgirl.com', + 'fmguy.com', + 'fnbmail.co.za', + 'fnmail.com', + 'folkfan.com', + 'foodmail.com', + 'footard.com', + 'footballmail.com', + 'foothills.net', + 'for-president.com', + 'force9.co.uk', + 'forfree.at', + 'forgetmail.com', + 'fornow.eu', + 'forpresident.com', + 'fortuncity.com', + 'fortunecity.com', + 'forum.dk', + 'foxmail.com', + 'fr33mail.info', + 'francemel.fr', + 'free-email.ga', + 'free-online.net', + 'free-org.com', + 'free.com.pe', + 'free.fr', + 'freeaccess.nl', + 'freeaccount.com', + 'freeandsingle.com', + 'freedom.usa.com', + 'freedomlover.com', + 'freegates.be', + 'freeghana.com', + 'freelance-france.eu', + 'freeler.nl', + 'freemail.c3.hu', + 'freemail.com.au', + 'freemail.com.pk', + 'freemail.de', + 'freemail.et', + 'freemail.gr', + 'freemail.hu', + 'freemail.it', + 'freemail.lt', + 'freemail.ms', + 'freemail.nl', + 'freemail.org.mk', + 'freemails.ga', + 'freemeil.gq', + 'freenet.de', + 'freenet.kg', + 'freeola.com', + 'freeola.net', + 'freeserve.co.uk', + 'freestart.hu', + 'freesurf.fr', + 'freesurf.nl', + 'freeuk.com', + 'freeuk.net', + 'freeukisp.co.uk', + 'freeweb.org', + 'freewebemail.com', + 'freeyellow.com', + 'freezone.co.uk', + 'fresnomail.com', + 'freudenkinder.de', + 'freundin.ru', + 'friendlymail.co.uk', + 'friends-cafe.com', + 'friendsfan.com', + 'from-africa.com', + 'from-america.com', + 'from-argentina.com', + 'from-asia.com', + 'from-australia.com', + 'from-belgium.com', + 'from-brazil.com', + 'from-canada.com', + 'from-china.net', + 'from-england.com', + 'from-europe.com', + 'from-france.net', + 'from-germany.net', + 'from-holland.com', + 'from-israel.com', + 'from-italy.net', + 'from-japan.net', + 'from-korea.com', + 'from-mexico.com', + 'from-outerspace.com', + 'from-russia.com', + 'from-spain.net', + 'fromalabama.com', + 'fromalaska.com', + 'fromarizona.com', + 'fromarkansas.com', + 'fromcalifornia.com', + 'fromcolorado.com', + 'fromconnecticut.com', + 'fromdelaware.com', + 'fromflorida.net', + 'fromgeorgia.com', + 'fromhawaii.net', + 'fromidaho.com', + 'fromillinois.com', + 'fromindiana.com', + 'fromiowa.com', + 'fromjupiter.com', + 'fromkansas.com', + 'fromkentucky.com', + 'fromlouisiana.com', + 'frommaine.net', + 'frommaryland.com', + 'frommassachusetts.com', + 'frommiami.com', + 'frommichigan.com', + 'fromminnesota.com', + 'frommississippi.com', + 'frommissouri.com', + 'frommontana.com', + 'fromnebraska.com', + 'fromnevada.com', + 'fromnewhampshire.com', + 'fromnewjersey.com', + 'fromnewmexico.com', + 'fromnewyork.net', + 'fromnorthcarolina.com', + 'fromnorthdakota.com', + 'fromohio.com', + 'fromoklahoma.com', + 'fromoregon.net', + 'frompennsylvania.com', + 'fromrhodeisland.com', + 'fromru.com', + 'fromsouthcarolina.com', + 'fromsouthdakota.com', + 'fromtennessee.com', + 'fromtexas.com', + 'fromthestates.com', + 'fromutah.com', + 'fromvermont.com', + 'fromvirginia.com', + 'fromwashington.com', + 'fromwashingtondc.com', + 'fromwestvirginia.com', + 'fromwisconsin.com', + 'fromwyoming.com', + 'front.ru', + 'frontier.com', + 'frontiernet.net', + 'frostbyte.uk.net', + 'fsmail.net', + 'ftc-i.net', + 'ftml.net', + 'fullmail.com', + 'funkfan.com', + 'fuorissimo.com', + 'furnitureprovider.com', + 'fuse.net', + 'fut.es', + 'fux0ringduh.com', + 'fwnb.com', + 'fxsmails.com', + 'galaxy5.com', + 'galaxyhit.com', + 'gamebox.net', + 'gamegeek.com', + 'gamespotmail.com', + 'gamno.config.work', + 'garbage.com', + 'gardener.com', + 'gawab.com', + 'gaybrighton.co.uk', + 'gaza.net', + 'gazeta.pl', + 'gazibooks.com', + 'gci.net', + 'geecities.com', + 'geek.com', + 'geek.hu', + 'geeklife.com', + 'gelitik.in', + 'gencmail.com', + 'general-hospital.com', + 'gentlemansclub.de', + 'geocities.com', + 'geography.net', + 'geologist.com', + 'geopia.com', + 'germanymail.com', + 'get.pp.ua', + 'get1mail.com', + 'getairmail.cf', + 'getairmail.com', + 'getairmail.ga', + 'getairmail.gq', + 'getonemail.net', + 'ghanamail.com', + 'ghostmail.com', + 'ghosttexter.de', + 'giga4u.de', + 'gigileung.org', + 'girl4god.com', + 'givepeaceachance.com', + 'glay.org', + 'glendale.net', + 'globalfree.it', + 'globalpagan.com', + 'globalsite.com.br', + 'gmail.com', + 'gmail.com.br', + 'gmail.ru', + 'gmx.at', + 'gmx.ch', + 'gmx.com', + 'gmx.de', + 'gmx.li', + 'gmx.net', + 'go.com', + 'go.ro', + 'go.ru', + 'go2net.com', + 'gocollege.com', + 'gocubs.com', + 'goemailgo.com', + 'gofree.co.uk', + 'gol.com', + 'goldenmail.ru', + 'goldmail.ru', + 'goldtoolbox.com', + 'golfemail.com', + 'golfilla.info', + 'golfmail.be', + 'gonavy.net', + 'goodnewsmail.com', + 'goodstick.com', + 'googlegroups.com', + 'googlemail.com', + 'goplay.com', + 'gorillaswithdirtyarmpits.com', + 'gorontalo.net', + 'gospelfan.com', + 'gothere.uk.com', + 'gotmail.com', + 'gotmail.org', + 'gotomy.com', + 'gotti.otherinbox.com', + 'gportal.hu', + 'graduate.org', + 'graffiti.net', + 'gramszu.net', + 'grandmamail.com', + 'grandmasmail.com', + 'graphic-designer.com', + 'grapplers.com', + 'gratisweb.com', + 'greenmail.net', + 'groupmail.com', + 'grr.la', + 'grungecafe.com', + 'gtmc.net', + 'gua.net', + 'guerrillamail.com', + 'guessmail.com', + 'guju.net', + 'gustr.com', + 'guy.com', + 'guy2.com', + 'guyanafriends.com', + 'gyorsposta.com', + 'gyorsposta.hu', + 'h-mail.us', + 'hab-verschlafen.de', + 'habmalnefrage.de', + 'hacccc.com', + 'hackermail.com', + 'hackermail.net', + 'hailmail.net', + 'hairdresser.net', + 'hamptonroads.com', + 'handbag.com', + 'handleit.com', + 'hang-ten.com', + 'hanmail.net', + 'happemail.com', + 'happycounsel.com', + 'happypuppy.com', + 'harakirimail.com', + 'hardcorefreak.com', + 'hartbot.de', + 'hawaii.rr.com', + 'hawaiiantel.net', + 'heartthrob.com', + 'heerschap.com', + 'heesun.net', + 'hehe.com', + 'hello.hu', + 'hello.net.au', + 'hello.to', + 'helter-skelter.com', + 'herediano.com', + 'herono1.com', + 'herp.in', + 'herr-der-mails.de', + 'hetnet.nl', + 'hey.to', + 'hhdevel.com', + 'hidzz.com', + 'highmilton.com', + 'highquality.com', + 'highveldmail.co.za', + 'hilarious.com', + 'hiphopfan.com', + 'hispavista.com', + 'hitmail.com', + 'hitthe.net', + 'hkg.net', + 'hkstarphoto.com', + 'hockeymail.com', + 'hollywoodkids.com', + 'home-email.com', + 'home.de', + 'home.nl', + 'home.no.net', + 'home.ro', + 'home.se', + 'homelocator.com', + 'homemail.com', + 'homestead.com', + 'honduras.com', + 'hongkong.com', + 'hookup.net', + 'hoopsmail.com', + 'hopemail.biz', + 'horrormail.com', + 'hot-mail.gq', + 'hot-shot.com', + 'hot.ee', + 'hotbot.com', + 'hotbrev.com', + 'hotfire.net', + 'hotletter.com', + 'hotmail.ca', + 'hotmail.ch', + 'hotmail.co', + 'hotmail.co.il', + 'hotmail.co.jp', + 'hotmail.co.nz', + 'hotmail.co.uk', + 'hotmail.co.za', + 'hotmail.com', + 'hotmail.com.au', + 'hotmail.com.br', + 'hotmail.com.tr', + 'hotmail.de', + 'hotmail.es', + 'hotmail.fi', + 'hotmail.fr', + 'hotmail.it', + 'hotmail.kg', + 'hotmail.kz', + 'hotmail.nl', + 'hotmail.ru', + 'hotmail.se', + 'hotpop.com', + 'hotpop3.com', + 'hotvoice.com', + 'housemail.com', + 'hsuchi.net', + 'hu2.ru', + 'hughes.net', + 'humanoid.net', + 'humn.ws.gy', + 'hunsa.com', + 'hurting.com', + 'hush.com', + 'hushmail.com', + 'hypernautica.com', + 'i-connect.com', + 'i-france.com', + 'i-mail.com.au', + 'i-p.com', + 'i.am', + 'i.ua', + 'i12.com', + 'i2pmail.org', + 'iamawoman.com', + 'iamwaiting.com', + 'iamwasted.com', + 'iamyours.com', + 'icestorm.com', + 'ich-bin-verrueckt-nach-dir.de', + 'ich-will-net.de', + 'icloud.com', + 'icmsconsultants.com', + 'icq.com', + 'icqmail.com', + 'icrazy.com', + 'id-base.com', + 'ididitmyway.com', + 'idigjesus.com', + 'idirect.com', + 'ieatspam.eu', + 'ieatspam.info', + 'ieh-mail.de', + 'iespana.es', + 'ifoward.com', + 'ig.com.br', + 'ignazio.it', + 'ignmail.com', + 'ihateclowns.com', + 'ihateyoualot.info', + 'iheartspam.org', + 'iinet.net.au', + 'ijustdontcare.com', + 'ikbenspamvrij.nl', + 'ilkposta.com', + 'ilovechocolate.com', + 'ilovejesus.com', + 'ilovetocollect.net', + 'ilse.nl', + 'imaginemail.com', + 'imail.ru', + 'imailbox.com', + 'imap-mail.com', + 'imap.cc', + 'imapmail.org', + 'imel.org', + 'imgof.com', + 'imgv.de', + 'immo-gerance.info', + 'imneverwrong.com', + 'imposter.co.uk', + 'imstations.com', + 'imstressed.com', + 'imtoosexy.com', + 'in-box.net', + 'in2jesus.com', + 'iname.com', + 'inbax.tk', + 'inbound.plus', + 'inbox.com', + 'inbox.net', + 'inbox.ru', + 'inbox.si', + 'inboxalias.com', + 'incamail.com', + 'incredimail.com', + 'indeedemail.com', + 'index.ua', + 'indexa.fr', + 'india.com', + 'indiatimes.com', + 'indo-mail.com', + 'indocities.com', + 'indomail.com', + 'indyracers.com', + 'inerted.com', + 'inet.com', + 'inet.net.au', + 'info-media.de', + 'info-radio.ml', + 'info66.com', + 'infohq.com', + 'infomail.es', + 'infomart.or.jp', + 'infospacemail.com', + 'infovia.com.ar', + 'inicia.es', + 'inmail.sk', + 'inmail24.com', + 'inmano.com', + 'inmynetwork.tk', + 'innocent.com', + 'inorbit.com', + 'inoutbox.com', + 'insidebaltimore.net', + 'insight.rr.com', + 'instant-mail.de', + 'instantemailaddress.com', + 'instantmail.fr', + 'instruction.com', + 'instructor.net', + 'insurer.com', + 'interburp.com', + 'interfree.it', + 'interia.pl', + 'interlap.com.ar', + 'intermail.co.il', + 'internet-e-mail.com', + 'internet-mail.org', + 'internet-police.com', + 'internetbiz.com', + 'internetdrive.com', + 'internetegypt.com', + 'internetemails.net', + 'internetmailing.net', + 'internode.on.net', + 'invalid.com', + 'inwind.it', + 'iobox.com', + 'iobox.fi', + 'iol.it', + 'iol.pt', + 'iowaemail.com', + 'ip3.com', + 'ip4.pp.ua', + 'ip6.pp.ua', + 'ipoo.org', + 'iprimus.com.au', + 'iqemail.com', + 'irangate.net', + 'iraqmail.com', + 'ireland.com', + 'irelandmail.com', + 'iremail.de', + 'irj.hu', + 'iroid.com', + 'isellcars.com', + 'iservejesus.com', + 'islamonline.net', + 'isleuthmail.com', + 'ismart.net', + 'isonfire.com', + 'isp9.net', + 'israelmail.com', + 'ist-allein.info', + 'ist-einmalig.de', + 'ist-ganz-allein.de', + 'ist-willig.de', + 'italymail.com', + 'itloox.com', + 'itmom.com', + 'ivebeenframed.com', + 'ivillage.com', + 'iwan-fals.com', + 'iwmail.com', + 'iwon.com', + 'izadpanah.com', + 'jahoopa.com', + 'jakuza.hu', + 'japan.com', + 'jaydemail.com', + 'jazzandjava.com', + 'jazzfan.com', + 'jazzgame.com', + 'je-recycle.info', + 'jerusalemmail.com', + 'jet-renovation.fr', + 'jetable.de', + 'jetable.pp.ua', + 'jetemail.net', + 'jippii.fi', + 'jmail.co.za', + 'job4u.com', + 'jobbikszimpatizans.hu', + 'joelonsoftware.com', + 'joinme.com', + 'jokes.com', + 'jordanmail.com', + 'journalist.com', + 'jourrapide.com', + 'jovem.te.pt', + 'joymail.com', + 'jpopmail.com', + 'jsrsolutions.com', + 'jubiimail.dk', + 'jump.com', + 'jumpy.it', + 'juniormail.com', + 'junk1e.com', + 'junkmail.com', + 'junkmail.gq', + 'juno.com', + 'justemail.net', + 'justicemail.com', + 'kaazoo.com', + 'kaffeeschluerfer.com', + 'kaffeeschluerfer.de', + 'kaixo.com', + 'kalpoint.com', + 'kansascity.com', + 'kapoorweb.com', + 'karachian.com', + 'karachioye.com', + 'karbasi.com', + 'katamail.com', + 'kayafmmail.co.za', + 'kbjrmail.com', + 'kcks.com', + 'keg-party.com', + 'keinpardon.de', + 'keko.com.ar', + 'kellychen.com', + 'keromail.com', + 'keyemail.com', + 'kgb.hu', + 'khosropour.com', + 'kickassmail.com', + 'killermail.com', + 'kimo.com', + 'kimsdisk.com', + 'kinglibrary.net', + 'kinki-kids.com', + 'kissfans.com', + 'kittymail.com', + 'kitznet.at', + 'kiwibox.com', + 'kiwitown.com', + 'klassmaster.net', + 'km.ru', + 'knol-power.nl', + 'kolumbus.fi', + 'kommespaeter.de', + 'konx.com', + 'korea.com', + 'koreamail.com', + 'kpnmail.nl', + 'krim.ws', + 'krongthip.com', + 'krunis.com', + 'ksanmail.com', + 'ksee24mail.com', + 'kube93mail.com', + 'kukamail.com', + 'kulturbetrieb.info', + 'kumarweb.com', + 'kuwait-mail.com', + 'l33r.eu', + 'la.com', + 'labetteraverouge.at', + 'ladymail.cz', + 'lagerlouts.com', + 'lags.us', + 'lahoreoye.com', + 'lakmail.com', + 'lamer.hu', + 'land.ru', + 'lankamail.com', + 'laoeq.com', + 'laposte.net', + 'lass-es-geschehen.de', + 'last-chance.pro', + 'lastmail.co', + 'latemodels.com', + 'latinmail.com', + 'lavache.com', + 'law.com', + 'lawyer.com', + 'lazyinbox.com', + 'leehom.net', + 'legalactions.com', + 'legalrc.loan', + 'legislator.com', + 'lenta.ru', + 'leonlai.net', + 'letsgomets.net', + 'letterboxes.org', + 'letthemeatspam.com', + 'levele.com', + 'levele.hu', + 'lex.bg', + 'lexis-nexis-mail.com', + 'libero.it', + 'liberomail.com', + 'lick101.com', + 'liebt-dich.info', + 'linkmaster.com', + 'linktrader.com', + 'linuxfreemail.com', + 'linuxmail.org', + 'lionsfan.com.au', + 'liontrucks.com', + 'liquidinformation.net', + 'list.ru', + 'listomail.com', + 'littleapple.com', + 'littleblueroom.com', + 'live.at', + 'live.be', + 'live.ca', + 'live.cl', + 'live.cn', + 'live.co.uk', + 'live.co.za', + 'live.com', + 'live.com.ar', + 'live.com.au', + 'live.com.mx', + 'live.com.pt', + 'live.com.sg', + 'live.de', + 'live.dk', + 'live.fr', + 'live.ie', + 'live.in', + 'live.it', + 'live.jp', + 'live.nl', + 'live.no', + 'live.ru', + 'live.se', + 'liveradio.tk', + 'liverpoolfans.com', + 'llandudno.com', + 'llangollen.com', + 'lmxmail.sk', + 'lobbyist.com', + 'localbar.com', + 'locos.com', + 'login-email.ga', + 'loh.pp.ua', + 'lolfreak.net', + 'lolito.tk', + 'london.com', + 'looksmart.co.uk', + 'looksmart.com', + 'looksmart.com.au', + 'lopezclub.com', + 'louiskoo.com', + 'love.cz', + 'loveable.com', + 'lovecat.com', + 'lovefall.ml', + 'lovefootball.com', + 'lovelygirl.net', + 'lovemail.com', + 'lover-boy.com', + 'lovergirl.com', + 'lovesea.gq', + 'lovethebroncos.com', + 'lovethecowboys.com', + 'loveyouforever.de', + 'lovingjesus.com', + 'lowandslow.com', + 'lr7.us', + 'lroid.com', + 'luso.pt', + 'luukku.com', + 'luv2.us', + 'lvie.com.sg', + 'lycos.co.uk', + 'lycos.com', + 'lycos.es', + 'lycos.it', + 'lycos.ne.jp', + 'lycosmail.com', + 'm-a-i-l.com', + 'm-hmail.com', + 'm4.org', + 'm4ilweb.info', + 'mac.com', + 'macbox.com', + 'macfreak.com', + 'machinecandy.com', + 'macmail.com', + 'madcreations.com', + 'madonnafan.com', + 'madrid.com', + 'maennerversteherin.com', + 'maennerversteherin.de', + 'maffia.hu', + 'magicmail.co.za', + 'magspam.net', + 'mahmoodweb.com', + 'mail-awu.de', + 'mail-box.cz', + 'mail-center.com', + 'mail-central.com', + 'mail-easy.fr', + 'mail-filter.com', + 'mail-me.com', + 'mail-page.com', + 'mail-tester.com', + 'mail.austria.com', + 'mail.az', + 'mail.be', + 'mail.bg', + 'mail.bulgaria.com', + 'mail.by', + 'mail.co.za', + 'mail.com', + 'mail.com.tr', + 'mail.de', + 'mail.ee', + 'mail.entrepeneurmag.com', + 'mail.freetown.com', + 'mail.gr', + 'mail.hitthebeach.com', + 'mail.htl22.at', + 'mail.md', + 'mail.misterpinball.de', + 'mail.nu', + 'mail.org.uk', + 'mail.pf', + 'mail.pt', + 'mail.r-o-o-t.com', + 'mail.ru', + 'mail.sisna.com', + 'mail.svenz.eu', + 'mail.tm', + 'mail.usa.com', + 'mail.vasarhely.hu', + 'mail.wtf', + 'mail114.net', + 'mail15.com', + 'mail2007.com', + 'mail2aaron.com', + 'mail2abby.com', + 'mail2abc.com', + 'mail2actor.com', + 'mail2admiral.com', + 'mail2adorable.com', + 'mail2adoration.com', + 'mail2adore.com', + 'mail2adventure.com', + 'mail2aeolus.com', + 'mail2aether.com', + 'mail2affection.com', + 'mail2afghanistan.com', + 'mail2africa.com', + 'mail2agent.com', + 'mail2aha.com', + 'mail2ahoy.com', + 'mail2aim.com', + 'mail2air.com', + 'mail2airbag.com', + 'mail2airforce.com', + 'mail2airport.com', + 'mail2alabama.com', + 'mail2alan.com', + 'mail2alaska.com', + 'mail2albania.com', + 'mail2alcoholic.com', + 'mail2alec.com', + 'mail2alexa.com', + 'mail2algeria.com', + 'mail2alicia.com', + 'mail2alien.com', + 'mail2allan.com', + 'mail2allen.com', + 'mail2allison.com', + 'mail2alpha.com', + 'mail2alyssa.com', + 'mail2amanda.com', + 'mail2amazing.com', + 'mail2amber.com', + 'mail2america.com', + 'mail2american.com', + 'mail2andorra.com', + 'mail2andrea.com', + 'mail2andy.com', + 'mail2anesthesiologist.com', + 'mail2angela.com', + 'mail2angola.com', + 'mail2ann.com', + 'mail2anna.com', + 'mail2anne.com', + 'mail2anthony.com', + 'mail2anything.com', + 'mail2aphrodite.com', + 'mail2apollo.com', + 'mail2april.com', + 'mail2aquarius.com', + 'mail2arabia.com', + 'mail2arabic.com', + 'mail2architect.com', + 'mail2ares.com', + 'mail2argentina.com', + 'mail2aries.com', + 'mail2arizona.com', + 'mail2arkansas.com', + 'mail2armenia.com', + 'mail2army.com', + 'mail2arnold.com', + 'mail2art.com', + 'mail2artemus.com', + 'mail2arthur.com', + 'mail2artist.com', + 'mail2ashley.com', + 'mail2ask.com', + 'mail2astronomer.com', + 'mail2athena.com', + 'mail2athlete.com', + 'mail2atlas.com', + 'mail2atom.com', + 'mail2attitude.com', + 'mail2auction.com', + 'mail2aunt.com', + 'mail2australia.com', + 'mail2austria.com', + 'mail2azerbaijan.com', + 'mail2baby.com', + 'mail2bahamas.com', + 'mail2bahrain.com', + 'mail2ballerina.com', + 'mail2ballplayer.com', + 'mail2band.com', + 'mail2bangladesh.com', + 'mail2bank.com', + 'mail2banker.com', + 'mail2bankrupt.com', + 'mail2baptist.com', + 'mail2bar.com', + 'mail2barbados.com', + 'mail2barbara.com', + 'mail2barter.com', + 'mail2basketball.com', + 'mail2batter.com', + 'mail2beach.com', + 'mail2beast.com', + 'mail2beatles.com', + 'mail2beauty.com', + 'mail2becky.com', + 'mail2beijing.com', + 'mail2belgium.com', + 'mail2belize.com', + 'mail2ben.com', + 'mail2bernard.com', + 'mail2beth.com', + 'mail2betty.com', + 'mail2beverly.com', + 'mail2beyond.com', + 'mail2biker.com', + 'mail2bill.com', + 'mail2billionaire.com', + 'mail2billy.com', + 'mail2bio.com', + 'mail2biologist.com', + 'mail2black.com', + 'mail2blackbelt.com', + 'mail2blake.com', + 'mail2blind.com', + 'mail2blonde.com', + 'mail2blues.com', + 'mail2bob.com', + 'mail2bobby.com', + 'mail2bolivia.com', + 'mail2bombay.com', + 'mail2bonn.com', + 'mail2bookmark.com', + 'mail2boreas.com', + 'mail2bosnia.com', + 'mail2boston.com', + 'mail2botswana.com', + 'mail2bradley.com', + 'mail2brazil.com', + 'mail2breakfast.com', + 'mail2brian.com', + 'mail2bride.com', + 'mail2brittany.com', + 'mail2broker.com', + 'mail2brook.com', + 'mail2bruce.com', + 'mail2brunei.com', + 'mail2brunette.com', + 'mail2brussels.com', + 'mail2bryan.com', + 'mail2bug.com', + 'mail2bulgaria.com', + 'mail2business.com', + 'mail2buy.com', + 'mail2ca.com', + 'mail2california.com', + 'mail2calvin.com', + 'mail2cambodia.com', + 'mail2cameroon.com', + 'mail2canada.com', + 'mail2cancer.com', + 'mail2capeverde.com', + 'mail2capricorn.com', + 'mail2cardinal.com', + 'mail2cardiologist.com', + 'mail2care.com', + 'mail2caroline.com', + 'mail2carolyn.com', + 'mail2casey.com', + 'mail2cat.com', + 'mail2caterer.com', + 'mail2cathy.com', + 'mail2catlover.com', + 'mail2catwalk.com', + 'mail2cell.com', + 'mail2chad.com', + 'mail2champaign.com', + 'mail2charles.com', + 'mail2chef.com', + 'mail2chemist.com', + 'mail2cherry.com', + 'mail2chicago.com', + 'mail2chile.com', + 'mail2china.com', + 'mail2chinese.com', + 'mail2chocolate.com', + 'mail2christian.com', + 'mail2christie.com', + 'mail2christmas.com', + 'mail2christy.com', + 'mail2chuck.com', + 'mail2cindy.com', + 'mail2clark.com', + 'mail2classifieds.com', + 'mail2claude.com', + 'mail2cliff.com', + 'mail2clinic.com', + 'mail2clint.com', + 'mail2close.com', + 'mail2club.com', + 'mail2coach.com', + 'mail2coastguard.com', + 'mail2colin.com', + 'mail2college.com', + 'mail2colombia.com', + 'mail2color.com', + 'mail2colorado.com', + 'mail2columbia.com', + 'mail2comedian.com', + 'mail2composer.com', + 'mail2computer.com', + 'mail2computers.com', + 'mail2concert.com', + 'mail2congo.com', + 'mail2connect.com', + 'mail2connecticut.com', + 'mail2consultant.com', + 'mail2convict.com', + 'mail2cook.com', + 'mail2cool.com', + 'mail2cory.com', + 'mail2costarica.com', + 'mail2country.com', + 'mail2courtney.com', + 'mail2cowboy.com', + 'mail2cowgirl.com', + 'mail2craig.com', + 'mail2crave.com', + 'mail2crazy.com', + 'mail2create.com', + 'mail2croatia.com', + 'mail2cry.com', + 'mail2crystal.com', + 'mail2cuba.com', + 'mail2culture.com', + 'mail2curt.com', + 'mail2customs.com', + 'mail2cute.com', + 'mail2cutey.com', + 'mail2cynthia.com', + 'mail2cyprus.com', + 'mail2czechrepublic.com', + 'mail2dad.com', + 'mail2dale.com', + 'mail2dallas.com', + 'mail2dan.com', + 'mail2dana.com', + 'mail2dance.com', + 'mail2dancer.com', + 'mail2danielle.com', + 'mail2danny.com', + 'mail2darlene.com', + 'mail2darling.com', + 'mail2darren.com', + 'mail2daughter.com', + 'mail2dave.com', + 'mail2dawn.com', + 'mail2dc.com', + 'mail2dealer.com', + 'mail2deanna.com', + 'mail2dearest.com', + 'mail2debbie.com', + 'mail2debby.com', + 'mail2deer.com', + 'mail2delaware.com', + 'mail2delicious.com', + 'mail2demeter.com', + 'mail2democrat.com', + 'mail2denise.com', + 'mail2denmark.com', + 'mail2dennis.com', + 'mail2dentist.com', + 'mail2derek.com', + 'mail2desert.com', + 'mail2devoted.com', + 'mail2devotion.com', + 'mail2diamond.com', + 'mail2diana.com', + 'mail2diane.com', + 'mail2diehard.com', + 'mail2dilemma.com', + 'mail2dillon.com', + 'mail2dinner.com', + 'mail2dinosaur.com', + 'mail2dionysos.com', + 'mail2diplomat.com', + 'mail2director.com', + 'mail2dirk.com', + 'mail2disco.com', + 'mail2dive.com', + 'mail2diver.com', + 'mail2divorced.com', + 'mail2djibouti.com', + 'mail2doctor.com', + 'mail2doglover.com', + 'mail2dominic.com', + 'mail2dominica.com', + 'mail2dominicanrepublic.com', + 'mail2don.com', + 'mail2donald.com', + 'mail2donna.com', + 'mail2doris.com', + 'mail2dorothy.com', + 'mail2doug.com', + 'mail2dough.com', + 'mail2douglas.com', + 'mail2dow.com', + 'mail2downtown.com', + 'mail2dream.com', + 'mail2dreamer.com', + 'mail2dude.com', + 'mail2dustin.com', + 'mail2dyke.com', + 'mail2dylan.com', + 'mail2earl.com', + 'mail2earth.com', + 'mail2eastend.com', + 'mail2eat.com', + 'mail2economist.com', + 'mail2ecuador.com', + 'mail2eddie.com', + 'mail2edgar.com', + 'mail2edwin.com', + 'mail2egypt.com', + 'mail2electron.com', + 'mail2eli.com', + 'mail2elizabeth.com', + 'mail2ellen.com', + 'mail2elliot.com', + 'mail2elsalvador.com', + 'mail2elvis.com', + 'mail2emergency.com', + 'mail2emily.com', + 'mail2engineer.com', + 'mail2english.com', + 'mail2environmentalist.com', + 'mail2eos.com', + 'mail2eric.com', + 'mail2erica.com', + 'mail2erin.com', + 'mail2erinyes.com', + 'mail2eris.com', + 'mail2eritrea.com', + 'mail2ernie.com', + 'mail2eros.com', + 'mail2estonia.com', + 'mail2ethan.com', + 'mail2ethiopia.com', + 'mail2eu.com', + 'mail2europe.com', + 'mail2eurus.com', + 'mail2eva.com', + 'mail2evan.com', + 'mail2evelyn.com', + 'mail2everything.com', + 'mail2exciting.com', + 'mail2expert.com', + 'mail2fairy.com', + 'mail2faith.com', + 'mail2fanatic.com', + 'mail2fancy.com', + 'mail2fantasy.com', + 'mail2farm.com', + 'mail2farmer.com', + 'mail2fashion.com', + 'mail2fat.com', + 'mail2feeling.com', + 'mail2female.com', + 'mail2fever.com', + 'mail2fighter.com', + 'mail2fiji.com', + 'mail2filmfestival.com', + 'mail2films.com', + 'mail2finance.com', + 'mail2finland.com', + 'mail2fireman.com', + 'mail2firm.com', + 'mail2fisherman.com', + 'mail2flexible.com', + 'mail2florence.com', + 'mail2florida.com', + 'mail2floyd.com', + 'mail2fly.com', + 'mail2fond.com', + 'mail2fondness.com', + 'mail2football.com', + 'mail2footballfan.com', + 'mail2found.com', + 'mail2france.com', + 'mail2frank.com', + 'mail2frankfurt.com', + 'mail2franklin.com', + 'mail2fred.com', + 'mail2freddie.com', + 'mail2free.com', + 'mail2freedom.com', + 'mail2french.com', + 'mail2freudian.com', + 'mail2friendship.com', + 'mail2from.com', + 'mail2fun.com', + 'mail2gabon.com', + 'mail2gabriel.com', + 'mail2gail.com', + 'mail2galaxy.com', + 'mail2gambia.com', + 'mail2games.com', + 'mail2gary.com', + 'mail2gavin.com', + 'mail2gemini.com', + 'mail2gene.com', + 'mail2genes.com', + 'mail2geneva.com', + 'mail2george.com', + 'mail2georgia.com', + 'mail2gerald.com', + 'mail2german.com', + 'mail2germany.com', + 'mail2ghana.com', + 'mail2gilbert.com', + 'mail2gina.com', + 'mail2girl.com', + 'mail2glen.com', + 'mail2gloria.com', + 'mail2goddess.com', + 'mail2gold.com', + 'mail2golfclub.com', + 'mail2golfer.com', + 'mail2gordon.com', + 'mail2government.com', + 'mail2grab.com', + 'mail2grace.com', + 'mail2graham.com', + 'mail2grandma.com', + 'mail2grandpa.com', + 'mail2grant.com', + 'mail2greece.com', + 'mail2green.com', + 'mail2greg.com', + 'mail2grenada.com', + 'mail2gsm.com', + 'mail2guard.com', + 'mail2guatemala.com', + 'mail2guy.com', + 'mail2hades.com', + 'mail2haiti.com', + 'mail2hal.com', + 'mail2handhelds.com', + 'mail2hank.com', + 'mail2hannah.com', + 'mail2harold.com', + 'mail2harry.com', + 'mail2hawaii.com', + 'mail2headhunter.com', + 'mail2heal.com', + 'mail2heather.com', + 'mail2heaven.com', + 'mail2hebe.com', + 'mail2hecate.com', + 'mail2heidi.com', + 'mail2helen.com', + 'mail2hell.com', + 'mail2help.com', + 'mail2helpdesk.com', + 'mail2henry.com', + 'mail2hephaestus.com', + 'mail2hera.com', + 'mail2hercules.com', + 'mail2herman.com', + 'mail2hermes.com', + 'mail2hespera.com', + 'mail2hestia.com', + 'mail2highschool.com', + 'mail2hindu.com', + 'mail2hip.com', + 'mail2hiphop.com', + 'mail2holland.com', + 'mail2holly.com', + 'mail2hollywood.com', + 'mail2homer.com', + 'mail2honduras.com', + 'mail2honey.com', + 'mail2hongkong.com', + 'mail2hope.com', + 'mail2horse.com', + 'mail2hot.com', + 'mail2hotel.com', + 'mail2houston.com', + 'mail2howard.com', + 'mail2hugh.com', + 'mail2human.com', + 'mail2hungary.com', + 'mail2hungry.com', + 'mail2hygeia.com', + 'mail2hyperspace.com', + 'mail2hypnos.com', + 'mail2ian.com', + 'mail2ice-cream.com', + 'mail2iceland.com', + 'mail2idaho.com', + 'mail2idontknow.com', + 'mail2illinois.com', + 'mail2imam.com', + 'mail2in.com', + 'mail2india.com', + 'mail2indian.com', + 'mail2indiana.com', + 'mail2indonesia.com', + 'mail2infinity.com', + 'mail2intense.com', + 'mail2iowa.com', + 'mail2iran.com', + 'mail2iraq.com', + 'mail2ireland.com', + 'mail2irene.com', + 'mail2iris.com', + 'mail2irresistible.com', + 'mail2irving.com', + 'mail2irwin.com', + 'mail2isaac.com', + 'mail2israel.com', + 'mail2italian.com', + 'mail2italy.com', + 'mail2jackie.com', + 'mail2jacob.com', + 'mail2jail.com', + 'mail2jaime.com', + 'mail2jake.com', + 'mail2jamaica.com', + 'mail2james.com', + 'mail2jamie.com', + 'mail2jan.com', + 'mail2jane.com', + 'mail2janet.com', + 'mail2janice.com', + 'mail2japan.com', + 'mail2japanese.com', + 'mail2jasmine.com', + 'mail2jason.com', + 'mail2java.com', + 'mail2jay.com', + 'mail2jazz.com', + 'mail2jed.com', + 'mail2jeffrey.com', + 'mail2jennifer.com', + 'mail2jenny.com', + 'mail2jeremy.com', + 'mail2jerry.com', + 'mail2jessica.com', + 'mail2jessie.com', + 'mail2jesus.com', + 'mail2jew.com', + 'mail2jeweler.com', + 'mail2jim.com', + 'mail2jimmy.com', + 'mail2joan.com', + 'mail2joann.com', + 'mail2joanna.com', + 'mail2jody.com', + 'mail2joe.com', + 'mail2joel.com', + 'mail2joey.com', + 'mail2john.com', + 'mail2join.com', + 'mail2jon.com', + 'mail2jonathan.com', + 'mail2jones.com', + 'mail2jordan.com', + 'mail2joseph.com', + 'mail2josh.com', + 'mail2joy.com', + 'mail2juan.com', + 'mail2judge.com', + 'mail2judy.com', + 'mail2juggler.com', + 'mail2julian.com', + 'mail2julie.com', + 'mail2jumbo.com', + 'mail2junk.com', + 'mail2justin.com', + 'mail2justme.com', + 'mail2k.ru', + 'mail2kansas.com', + 'mail2karate.com', + 'mail2karen.com', + 'mail2karl.com', + 'mail2karma.com', + 'mail2kathleen.com', + 'mail2kathy.com', + 'mail2katie.com', + 'mail2kay.com', + 'mail2kazakhstan.com', + 'mail2keen.com', + 'mail2keith.com', + 'mail2kelly.com', + 'mail2kelsey.com', + 'mail2ken.com', + 'mail2kendall.com', + 'mail2kennedy.com', + 'mail2kenneth.com', + 'mail2kenny.com', + 'mail2kentucky.com', + 'mail2kenya.com', + 'mail2kerry.com', + 'mail2kevin.com', + 'mail2kim.com', + 'mail2kimberly.com', + 'mail2king.com', + 'mail2kirk.com', + 'mail2kiss.com', + 'mail2kosher.com', + 'mail2kristin.com', + 'mail2kurt.com', + 'mail2kuwait.com', + 'mail2kyle.com', + 'mail2kyrgyzstan.com', + 'mail2la.com', + 'mail2lacrosse.com', + 'mail2lance.com', + 'mail2lao.com', + 'mail2larry.com', + 'mail2latvia.com', + 'mail2laugh.com', + 'mail2laura.com', + 'mail2lauren.com', + 'mail2laurie.com', + 'mail2lawrence.com', + 'mail2lawyer.com', + 'mail2lebanon.com', + 'mail2lee.com', + 'mail2leo.com', + 'mail2leon.com', + 'mail2leonard.com', + 'mail2leone.com', + 'mail2leslie.com', + 'mail2letter.com', + 'mail2liberia.com', + 'mail2libertarian.com', + 'mail2libra.com', + 'mail2libya.com', + 'mail2liechtenstein.com', + 'mail2life.com', + 'mail2linda.com', + 'mail2linux.com', + 'mail2lionel.com', + 'mail2lipstick.com', + 'mail2liquid.com', + 'mail2lisa.com', + 'mail2lithuania.com', + 'mail2litigator.com', + 'mail2liz.com', + 'mail2lloyd.com', + 'mail2lois.com', + 'mail2lola.com', + 'mail2london.com', + 'mail2looking.com', + 'mail2lori.com', + 'mail2lost.com', + 'mail2lou.com', + 'mail2louis.com', + 'mail2louisiana.com', + 'mail2lovable.com', + 'mail2love.com', + 'mail2lucky.com', + 'mail2lucy.com', + 'mail2lunch.com', + 'mail2lust.com', + 'mail2luxembourg.com', + 'mail2luxury.com', + 'mail2lyle.com', + 'mail2lynn.com', + 'mail2madagascar.com', + 'mail2madison.com', + 'mail2madrid.com', + 'mail2maggie.com', + 'mail2mail4.com', + 'mail2maine.com', + 'mail2malawi.com', + 'mail2malaysia.com', + 'mail2maldives.com', + 'mail2mali.com', + 'mail2malta.com', + 'mail2mambo.com', + 'mail2man.com', + 'mail2mandy.com', + 'mail2manhunter.com', + 'mail2mankind.com', + 'mail2many.com', + 'mail2marc.com', + 'mail2marcia.com', + 'mail2margaret.com', + 'mail2margie.com', + 'mail2marhaba.com', + 'mail2maria.com', + 'mail2marilyn.com', + 'mail2marines.com', + 'mail2mark.com', + 'mail2marriage.com', + 'mail2married.com', + 'mail2marries.com', + 'mail2mars.com', + 'mail2marsha.com', + 'mail2marshallislands.com', + 'mail2martha.com', + 'mail2martin.com', + 'mail2marty.com', + 'mail2marvin.com', + 'mail2mary.com', + 'mail2maryland.com', + 'mail2mason.com', + 'mail2massachusetts.com', + 'mail2matt.com', + 'mail2matthew.com', + 'mail2maurice.com', + 'mail2mauritania.com', + 'mail2mauritius.com', + 'mail2max.com', + 'mail2maxwell.com', + 'mail2maybe.com', + 'mail2mba.com', + 'mail2me4u.com', + 'mail2mechanic.com', + 'mail2medieval.com', + 'mail2megan.com', + 'mail2mel.com', + 'mail2melanie.com', + 'mail2melissa.com', + 'mail2melody.com', + 'mail2member.com', + 'mail2memphis.com', + 'mail2methodist.com', + 'mail2mexican.com', + 'mail2mexico.com', + 'mail2mgz.com', + 'mail2miami.com', + 'mail2michael.com', + 'mail2michelle.com', + 'mail2michigan.com', + 'mail2mike.com', + 'mail2milan.com', + 'mail2milano.com', + 'mail2mildred.com', + 'mail2milkyway.com', + 'mail2millennium.com', + 'mail2millionaire.com', + 'mail2milton.com', + 'mail2mime.com', + 'mail2mindreader.com', + 'mail2mini.com', + 'mail2minister.com', + 'mail2minneapolis.com', + 'mail2minnesota.com', + 'mail2miracle.com', + 'mail2missionary.com', + 'mail2mississippi.com', + 'mail2missouri.com', + 'mail2mitch.com', + 'mail2model.com', + 'mail2moldova.commail2molly.com', + 'mail2mom.com', + 'mail2monaco.com', + 'mail2money.com', + 'mail2mongolia.com', + 'mail2monica.com', + 'mail2montana.com', + 'mail2monty.com', + 'mail2moon.com', + 'mail2morocco.com', + 'mail2morpheus.com', + 'mail2mors.com', + 'mail2moscow.com', + 'mail2moslem.com', + 'mail2mouseketeer.com', + 'mail2movies.com', + 'mail2mozambique.com', + 'mail2mp3.com', + 'mail2mrright.com', + 'mail2msright.com', + 'mail2museum.com', + 'mail2music.com', + 'mail2musician.com', + 'mail2muslim.com', + 'mail2my.com', + 'mail2myboat.com', + 'mail2mycar.com', + 'mail2mycell.com', + 'mail2mygsm.com', + 'mail2mylaptop.com', + 'mail2mymac.com', + 'mail2mypager.com', + 'mail2mypalm.com', + 'mail2mypc.com', + 'mail2myphone.com', + 'mail2myplane.com', + 'mail2namibia.com', + 'mail2nancy.com', + 'mail2nasdaq.com', + 'mail2nathan.com', + 'mail2nauru.com', + 'mail2navy.com', + 'mail2neal.com', + 'mail2nebraska.com', + 'mail2ned.com', + 'mail2neil.com', + 'mail2nelson.com', + 'mail2nemesis.com', + 'mail2nepal.com', + 'mail2netherlands.com', + 'mail2network.com', + 'mail2nevada.com', + 'mail2newhampshire.com', + 'mail2newjersey.com', + 'mail2newmexico.com', + 'mail2newyork.com', + 'mail2newzealand.com', + 'mail2nicaragua.com', + 'mail2nick.com', + 'mail2nicole.com', + 'mail2niger.com', + 'mail2nigeria.com', + 'mail2nike.com', + 'mail2no.com', + 'mail2noah.com', + 'mail2noel.com', + 'mail2noelle.com', + 'mail2normal.com', + 'mail2norman.com', + 'mail2northamerica.com', + 'mail2northcarolina.com', + 'mail2northdakota.com', + 'mail2northpole.com', + 'mail2norway.com', + 'mail2notus.com', + 'mail2noway.com', + 'mail2nowhere.com', + 'mail2nuclear.com', + 'mail2nun.com', + 'mail2ny.com', + 'mail2oasis.com', + 'mail2oceanographer.com', + 'mail2ohio.com', + 'mail2ok.com', + 'mail2oklahoma.com', + 'mail2oliver.com', + 'mail2oman.com', + 'mail2one.com', + 'mail2onfire.com', + 'mail2online.com', + 'mail2oops.com', + 'mail2open.com', + 'mail2ophthalmologist.com', + 'mail2optometrist.com', + 'mail2oregon.com', + 'mail2oscars.com', + 'mail2oslo.com', + 'mail2painter.com', + 'mail2pakistan.com', + 'mail2palau.com', + 'mail2pan.com', + 'mail2panama.com', + 'mail2paraguay.com', + 'mail2paralegal.com', + 'mail2paris.com', + 'mail2park.com', + 'mail2parker.com', + 'mail2party.com', + 'mail2passion.com', + 'mail2pat.com', + 'mail2patricia.com', + 'mail2patrick.com', + 'mail2patty.com', + 'mail2paul.com', + 'mail2paula.com', + 'mail2pay.com', + 'mail2peace.com', + 'mail2pediatrician.com', + 'mail2peggy.com', + 'mail2pennsylvania.com', + 'mail2perry.com', + 'mail2persephone.com', + 'mail2persian.com', + 'mail2peru.com', + 'mail2pete.com', + 'mail2peter.com', + 'mail2pharmacist.com', + 'mail2phil.com', + 'mail2philippines.com', + 'mail2phoenix.com', + 'mail2phonecall.com', + 'mail2phyllis.com', + 'mail2pickup.com', + 'mail2pilot.com', + 'mail2pisces.com', + 'mail2planet.com', + 'mail2platinum.com', + 'mail2plato.com', + 'mail2pluto.com', + 'mail2pm.com', + 'mail2podiatrist.com', + 'mail2poet.com', + 'mail2poland.com', + 'mail2policeman.com', + 'mail2policewoman.com', + 'mail2politician.com', + 'mail2pop.com', + 'mail2pope.com', + 'mail2popular.com', + 'mail2portugal.com', + 'mail2poseidon.com', + 'mail2potatohead.com', + 'mail2power.com', + 'mail2presbyterian.com', + 'mail2president.com', + 'mail2priest.com', + 'mail2prince.com', + 'mail2princess.com', + 'mail2producer.com', + 'mail2professor.com', + 'mail2protect.com', + 'mail2psychiatrist.com', + 'mail2psycho.com', + 'mail2psychologist.com', + 'mail2qatar.com', + 'mail2queen.com', + 'mail2rabbi.com', + 'mail2race.com', + 'mail2racer.com', + 'mail2rachel.com', + 'mail2rage.com', + 'mail2rainmaker.com', + 'mail2ralph.com', + 'mail2randy.com', + 'mail2rap.com', + 'mail2rare.com', + 'mail2rave.com', + 'mail2ray.com', + 'mail2raymond.com', + 'mail2realtor.com', + 'mail2rebecca.com', + 'mail2recruiter.com', + 'mail2recycle.com', + 'mail2redhead.com', + 'mail2reed.com', + 'mail2reggie.com', + 'mail2register.com', + 'mail2rent.com', + 'mail2republican.com', + 'mail2resort.com', + 'mail2rex.com', + 'mail2rhodeisland.com', + 'mail2rich.com', + 'mail2richard.com', + 'mail2ricky.com', + 'mail2ride.com', + 'mail2riley.com', + 'mail2rita.com', + 'mail2rob.com', + 'mail2robert.com', + 'mail2roberta.com', + 'mail2robin.com', + 'mail2rock.com', + 'mail2rocker.com', + 'mail2rod.com', + 'mail2rodney.com', + 'mail2romania.com', + 'mail2rome.com', + 'mail2ron.com', + 'mail2ronald.com', + 'mail2ronnie.com', + 'mail2rose.com', + 'mail2rosie.com', + 'mail2roy.com', + 'mail2rss.org', + 'mail2rudy.com', + 'mail2rugby.com', + 'mail2runner.com', + 'mail2russell.com', + 'mail2russia.com', + 'mail2russian.com', + 'mail2rusty.com', + 'mail2ruth.com', + 'mail2rwanda.com', + 'mail2ryan.com', + 'mail2sa.com', + 'mail2sabrina.com', + 'mail2safe.com', + 'mail2sagittarius.com', + 'mail2sail.com', + 'mail2sailor.com', + 'mail2sal.com', + 'mail2salaam.com', + 'mail2sam.com', + 'mail2samantha.com', + 'mail2samoa.com', + 'mail2samurai.com', + 'mail2sandra.com', + 'mail2sandy.com', + 'mail2sanfrancisco.com', + 'mail2sanmarino.com', + 'mail2santa.com', + 'mail2sara.com', + 'mail2sarah.com', + 'mail2sat.com', + 'mail2saturn.com', + 'mail2saudi.com', + 'mail2saudiarabia.com', + 'mail2save.com', + 'mail2savings.com', + 'mail2school.com', + 'mail2scientist.com', + 'mail2scorpio.com', + 'mail2scott.com', + 'mail2sean.com', + 'mail2search.com', + 'mail2seattle.com', + 'mail2secretagent.com', + 'mail2senate.com', + 'mail2senegal.com', + 'mail2sensual.com', + 'mail2seth.com', + 'mail2sevenseas.com', + 'mail2sexy.com', + 'mail2seychelles.com', + 'mail2shane.com', + 'mail2sharon.com', + 'mail2shawn.com', + 'mail2ship.com', + 'mail2shirley.com', + 'mail2shoot.com', + 'mail2shuttle.com', + 'mail2sierraleone.com', + 'mail2simon.com', + 'mail2singapore.com', + 'mail2single.com', + 'mail2site.com', + 'mail2skater.com', + 'mail2skier.com', + 'mail2sky.com', + 'mail2sleek.com', + 'mail2slim.com', + 'mail2slovakia.com', + 'mail2slovenia.com', + 'mail2smile.com', + 'mail2smith.com', + 'mail2smooth.com', + 'mail2soccer.com', + 'mail2soccerfan.com', + 'mail2socialist.com', + 'mail2soldier.com', + 'mail2somalia.com', + 'mail2son.com', + 'mail2song.com', + 'mail2sos.com', + 'mail2sound.com', + 'mail2southafrica.com', + 'mail2southamerica.com', + 'mail2southcarolina.com', + 'mail2southdakota.com', + 'mail2southkorea.com', + 'mail2southpole.com', + 'mail2spain.com', + 'mail2spanish.com', + 'mail2spare.com', + 'mail2spectrum.com', + 'mail2splash.com', + 'mail2sponsor.com', + 'mail2sports.com', + 'mail2srilanka.com', + 'mail2stacy.com', + 'mail2stan.com', + 'mail2stanley.com', + 'mail2star.com', + 'mail2state.com', + 'mail2stephanie.com', + 'mail2steve.com', + 'mail2steven.com', + 'mail2stewart.com', + 'mail2stlouis.com', + 'mail2stock.com', + 'mail2stockholm.com', + 'mail2stockmarket.com', + 'mail2storage.com', + 'mail2store.com', + 'mail2strong.com', + 'mail2student.com', + 'mail2studio.com', + 'mail2studio54.com', + 'mail2stuntman.com', + 'mail2subscribe.com', + 'mail2sudan.com', + 'mail2superstar.com', + 'mail2surfer.com', + 'mail2suriname.com', + 'mail2susan.com', + 'mail2suzie.com', + 'mail2swaziland.com', + 'mail2sweden.com', + 'mail2sweetheart.com', + 'mail2swim.com', + 'mail2swimmer.com', + 'mail2swiss.com', + 'mail2switzerland.com', + 'mail2sydney.com', + 'mail2sylvia.com', + 'mail2syria.com', + 'mail2taboo.com', + 'mail2taiwan.com', + 'mail2tajikistan.com', + 'mail2tammy.com', + 'mail2tango.com', + 'mail2tanya.com', + 'mail2tanzania.com', + 'mail2tara.com', + 'mail2taurus.com', + 'mail2taxi.com', + 'mail2taxidermist.com', + 'mail2taylor.com', + 'mail2taz.com', + 'mail2teacher.com', + 'mail2technician.com', + 'mail2ted.com', + 'mail2telephone.com', + 'mail2teletubbie.com', + 'mail2tenderness.com', + 'mail2tennessee.com', + 'mail2tennis.com', + 'mail2tennisfan.com', + 'mail2terri.com', + 'mail2terry.com', + 'mail2test.com', + 'mail2texas.com', + 'mail2thailand.com', + 'mail2therapy.com', + 'mail2think.com', + 'mail2tickets.com', + 'mail2tiffany.com', + 'mail2tim.com', + 'mail2time.com', + 'mail2timothy.com', + 'mail2tina.com', + 'mail2titanic.com', + 'mail2toby.com', + 'mail2todd.com', + 'mail2togo.com', + 'mail2tom.com', + 'mail2tommy.com', + 'mail2tonga.com', + 'mail2tony.com', + 'mail2touch.com', + 'mail2tourist.com', + 'mail2tracey.com', + 'mail2tracy.com', + 'mail2tramp.com', + 'mail2travel.com', + 'mail2traveler.com', + 'mail2travis.com', + 'mail2trekkie.com', + 'mail2trex.com', + 'mail2triallawyer.com', + 'mail2trick.com', + 'mail2trillionaire.com', + 'mail2troy.com', + 'mail2truck.com', + 'mail2trump.com', + 'mail2try.com', + 'mail2tunisia.com', + 'mail2turbo.com', + 'mail2turkey.com', + 'mail2turkmenistan.com', + 'mail2tv.com', + 'mail2tycoon.com', + 'mail2tyler.com', + 'mail2u4me.com', + 'mail2uae.com', + 'mail2uganda.com', + 'mail2uk.com', + 'mail2ukraine.com', + 'mail2uncle.com', + 'mail2unsubscribe.com', + 'mail2uptown.com', + 'mail2uruguay.com', + 'mail2usa.com', + 'mail2utah.com', + 'mail2uzbekistan.com', + 'mail2v.com', + 'mail2vacation.com', + 'mail2valentines.com', + 'mail2valerie.com', + 'mail2valley.com', + 'mail2vamoose.com', + 'mail2vanessa.com', + 'mail2vanuatu.com', + 'mail2venezuela.com', + 'mail2venous.com', + 'mail2venus.com', + 'mail2vermont.com', + 'mail2vickie.com', + 'mail2victor.com', + 'mail2victoria.com', + 'mail2vienna.com', + 'mail2vietnam.com', + 'mail2vince.com', + 'mail2virginia.com', + 'mail2virgo.com', + 'mail2visionary.com', + 'mail2vodka.com', + 'mail2volleyball.com', + 'mail2waiter.com', + 'mail2wallstreet.com', + 'mail2wally.com', + 'mail2walter.com', + 'mail2warren.com', + 'mail2washington.com', + 'mail2wave.com', + 'mail2way.com', + 'mail2waycool.com', + 'mail2wayne.com', + 'mail2webmaster.com', + 'mail2webtop.com', + 'mail2webtv.com', + 'mail2weird.com', + 'mail2wendell.com', + 'mail2wendy.com', + 'mail2westend.com', + 'mail2westvirginia.com', + 'mail2whether.com', + 'mail2whip.com', + 'mail2white.com', + 'mail2whitehouse.com', + 'mail2whitney.com', + 'mail2why.com', + 'mail2wilbur.com', + 'mail2wild.com', + 'mail2willard.com', + 'mail2willie.com', + 'mail2wine.com', + 'mail2winner.com', + 'mail2wired.com', + 'mail2wisconsin.com', + 'mail2woman.com', + 'mail2wonder.com', + 'mail2world.com', + 'mail2worship.com', + 'mail2wow.com', + 'mail2www.com', + 'mail2wyoming.com', + 'mail2xfiles.com', + 'mail2xox.com', + 'mail2yachtclub.com', + 'mail2yahalla.com', + 'mail2yemen.com', + 'mail2yes.com', + 'mail2yugoslavia.com', + 'mail2zack.com', + 'mail2zambia.com', + 'mail2zenith.com', + 'mail2zephir.com', + 'mail2zeus.com', + 'mail2zipper.com', + 'mail2zoo.com', + 'mail2zoologist.com', + 'mail2zurich.com', + 'mail3000.com', + 'mail333.com', + 'mail4trash.com', + 'mail4u.info', + 'mailandftp.com', + 'mailandnews.com', + 'mailas.com', + 'mailasia.com', + 'mailbolt.com', + 'mailbomb.net', + 'mailboom.com', + 'mailbox.as', + 'mailbox.co.za', + 'mailbox.gr', + 'mailbox.hu', + 'mailbox72.biz', + 'mailbox80.biz', + 'mailbr.com.br', + 'mailc.net', + 'mailcan.com', + 'mailcat.biz', + 'mailcc.com', + 'mailchoose.co', + 'mailcity.com', + 'mailclub.fr', + 'mailclub.net', + 'maildrop.cc', + 'maildrop.gq', + 'maildx.com', + 'mailed.ro', + 'mailexcite.com', + 'mailfa.tk', + 'mailfence.com', + 'mailforce.net', + 'mailforspam.com', + 'mailfree.gq', + 'mailfs.com', + 'mailftp.com', + 'mailgenie.net', + 'mailguard.me', + 'mailhaven.com', + 'mailhood.com', + 'mailimate.com', + 'mailinator.com', + 'mailinator.org', + 'mailinator.us', + 'mailinblack.com', + 'mailingaddress.org', + 'mailingweb.com', + 'mailisent.com', + 'mailismagic.com', + 'mailite.com', + 'mailmate.com', + 'mailme.dk', + 'mailme.gq', + 'mailme24.com', + 'mailmight.com', + 'mailmij.nl', + 'mailnator.com', + 'mailnew.com', + 'mailops.com', + 'mailoye.com', + 'mailpanda.com', + 'mailpick.biz', + 'mailpokemon.com', + 'mailpost.zzn.com', + 'mailpride.com', + 'mailproxsy.com', + 'mailpuppy.com', + 'mailquack.com', + 'mailrock.biz', + 'mailroom.com', + 'mailru.com', + 'mailsac.com', + 'mailseal.de', + 'mailsent.net', + 'mailservice.ms', + 'mailshuttle.com', + 'mailslapping.com', + 'mailstart.com', + 'mailstartplus.com', + 'mailsurf.com', + 'mailtag.com', + 'mailtemp.info', + 'mailto.de', + 'mailtothis.com', + 'mailueberfall.de', + 'mailup.net', + 'mailwire.com', + 'mailworks.org', + 'mailzi.ru', + 'mailzilla.org', + 'maktoob.com', + 'malayalamtelevision.net', + 'maltesemail.com', + 'mamber.net', + 'manager.de', + 'mancity.net', + 'mantrafreenet.com', + 'mantramail.com', + 'mantraonline.com', + 'manybrain.com', + 'marchmail.com', + 'mariahc.com', + 'marijuana.com', + 'marijuana.nl', + 'married-not.com', + 'marsattack.com', + 'martindalemail.com', + 'mash4077.com', + 'masrawy.com', + 'matmail.com', + 'mauimail.com', + 'mauritius.com', + 'maxleft.com', + 'maxmail.co.uk', + 'mbox.com.au', + 'mchsi.com', + 'me-mail.hu', + 'me.com', + 'medical.net.au', + 'medscape.com', + 'meetingmall.com', + 'megago.com', + 'megamail.pt', + 'megapoint.com', + 'mehrani.com', + 'mehtaweb.com', + 'meine-dateien.info', + 'meine-diashow.de', + 'meine-fotos.info', + 'meine-urlaubsfotos.de', + 'mekhong.com', + 'melodymail.com', + 'meloo.com', + 'merda.flu.cc', + 'merda.igg.biz', + 'merda.nut.cc', + 'merda.usa.cc', + 'message.hu', + 'message.myspace.com', + 'messages.to', + 'metacrawler.com', + 'metalfan.com', + 'metaping.com', + 'metta.lk', + 'mexicomail.com', + 'mezimages.net', + 'mfsa.ru', + 'mierdamail.com', + 'miesto.sk', + 'mighty.co.za', + 'migmail.net', + 'migmail.pl', + 'migumail.com', + 'miho-nakayama.com', + 'mikrotamanet.com', + 'millionaireintraining.com', + 'millionairemail.com', + 'milmail.com', + 'mindless.com', + 'mindspring.com', + 'minister.com', + 'misery.net', + 'mittalweb.com', + 'mixmail.com', + 'mjfrogmail.com', + 'ml1.net', + 'mlb.bounce.ed10.net', + 'mm.st', + 'mns.ru', + 'moakt.com', + 'mobilbatam.com', + 'mobileninja.co.uk', + 'mochamail.com', + 'mohammed.com', + 'mohmal.com', + 'moldova.cc', + 'moldova.com', + 'moldovacc.com', + 'momslife.com', + 'monemail.com', + 'money.net', + 'montevideo.com.uy', + 'monumentmail.com', + 'moonman.com', + 'moose-mail.com', + 'mor19.uu.gl', + 'mortaza.com', + 'mosaicfx.com', + 'moscowmail.com', + 'most-wanted.com', + 'mostlysunny.com', + 'motormania.com', + 'movemail.com', + 'movieluver.com', + 'mox.pp.ua', + 'mp4.it', + 'mr-potatohead.com', + 'mscold.com', + 'msgbox.com', + 'msn.cn', + 'msn.com', + 'msn.nl', + 'mt2015.com', + 'mt2016.com', + 'mttestdriver.com', + 'muehlacker.tk', + 'muell.icu', + 'muellemail.com', + 'muellmail.com', + 'mundomail.net', + 'munich.com', + 'music.com', + 'musician.org', + 'musicscene.org', + 'muskelshirt.de', + 'muslim.com', + 'muslimsonline.com', + 'mutantweb.com', + 'mvrht.com', + 'my.com', + 'my10minutemail.com', + 'mybox.it', + 'mycabin.com', + 'mycity.com', + 'mycool.com', + 'mydomain.com', + 'mydotcomaddress.com', + 'myfamily.com', + 'myfastmail.com', + 'mygo.com', + 'myiris.com', + 'mymacmail.com', + 'mynamedot.com', + 'mynet.com', + 'mynetaddress.com', + 'mynetstore.de', + 'myownemail.com', + 'myownfriends.com', + 'mypacks.net', + 'mypad.com', + 'mypersonalemail.com', + 'myplace.com', + 'myrambler.ru', + 'myrealbox.com', + 'myremarq.com', + 'myself.com', + 'myspaceinc.net', + 'myspamless.com', + 'mystupidjob.com', + 'mytemp.email', + 'mythirdage.com', + 'myway.com', + 'myworldmail.com', + 'n2.com', + 'n2baseball.com', + 'n2business.com', + 'n2mail.com', + 'n2soccer.com', + 'n2software.com', + 'nabc.biz', + 'nafe.com', + 'nagpal.net', + 'nakedgreens.com', + 'name.com', + 'nameplanet.com', + 'nandomail.com', + 'naplesnews.net', + 'naseej.com', + 'nativestar.net', + 'nativeweb.net', + 'naui.net', + 'naver.com', + 'navigator.lv', + 'navy.org', + 'naz.com', + 'nc.rr.com', + 'nchoicemail.com', + 'neeva.net', + 'nemra1.com', + 'nenter.com', + 'neo.rr.com', + 'nervhq.org', + 'net-c.be', + 'net-c.ca', + 'net-c.cat', + 'net-c.com', + 'net-c.es', + 'net-c.fr', + 'net-c.it', + 'net-c.lu', + 'net-c.nl', + 'net-c.pl', + 'net-pager.net', + 'net-shopping.com', + 'net4b.pt', + 'net4you.at', + 'netbounce.com', + 'netbroadcaster.com', + 'netby.dk', + 'netc.eu', + 'netc.fr', + 'netc.it', + 'netc.lu', + 'netc.pl', + 'netcenter-vn.net', + 'netcmail.com', + 'netcourrier.com', + 'netexecutive.com', + 'netexpressway.com', + 'netgenie.com', + 'netian.com', + 'netizen.com.ar', + 'netlane.com', + 'netlimit.com', + 'netmongol.com', + 'netnet.com.sg', + 'netnoir.net', + 'netpiper.com', + 'netposta.net', + 'netralink.com', + 'netscape.net', + 'netscapeonline.co.uk', + 'netspace.net.au', + 'netspeedway.com', + 'netsquare.com', + 'netster.com', + 'nettaxi.com', + 'nettemail.com', + 'netterchef.de', + 'netti.fi', + 'netzero.com', + 'netzero.net', + 'netzidiot.de', + 'neue-dateien.de', + 'neuro.md', + 'newmail.com', + 'newmail.net', + 'newmail.ru', + 'newsboysmail.com', + 'newyork.com', + 'nextmail.ru', + 'nexxmail.com', + 'nfmail.com', + 'nicebush.com', + 'nicegal.com', + 'nicholastse.net', + 'nicolastse.com', + 'nightmail.com', + 'nikopage.com', + 'nimail.com', + 'ninfan.com', + 'nirvanafan.com', + 'nmail.cf', + 'noavar.com', + 'nonpartisan.com', + 'nonspam.eu', + 'nonspammer.de', + 'norika-fujiwara.com', + 'norikomail.com', + 'northgates.net', + 'nospammail.net', + 'nospamthanks.info', + 'nowhere.org', + 'ntelos.net', + 'ntlhelp.net', + 'ntlworld.com', + 'ntscan.com', + 'null.net', + 'nullbox.info', + 'nur-fuer-spam.de', + 'nus.edu.sg', + 'nwldx.com', + 'nwytg.net', + 'nxt.ru', + 'ny.com', + 'nybella.com', + 'nyc.com', + 'nycmail.com', + 'nzoomail.com', + 'o-tay.com', + 'o2.co.uk', + 'oaklandas-fan.com', + 'oath.com', + 'oceanfree.net', + 'odaymail.com', + 'oddpost.com', + 'odmail.com', + 'office-dateien.de', + 'office-email.com', + 'offroadwarrior.com', + 'oicexchange.com', + 'oida.icu', + 'oikrach.com', + 'okbank.com', + 'okhuman.com', + 'okmad.com', + 'okmagic.com', + 'okname.net', + 'okuk.com', + 'oldies104mail.com', + 'ole.com', + 'olemail.com', + 'olympist.net', + 'olypmall.ru', + 'omaninfo.com', + 'omen.ru', + 'onebox.com', + 'onenet.com.ar', + 'oneoffmail.com', + 'onet.com.pl', + 'onet.eu', + 'onet.pl', + 'oninet.pt', + 'online.ie', + 'online.ms', + 'online.nl', + 'onlinewiz.com', + 'onmilwaukee.com', + 'onobox.com', + 'op.pl', + 'opayq.com', + 'openmailbox.org', + 'operafan.com', + 'operamail.com', + 'opoczta.pl', + 'optician.com', + 'optonline.net', + 'optusnet.com.au', + 'orange.fr', + 'orbitel.bg', + 'orgmail.net', + 'orthodontist.net', + 'osite.com.br', + 'oso.com', + 'otakumail.com', + 'our-computer.com', + 'our-office.com', + 'our.st', + 'ourbrisbane.com', + 'ourklips.com', + 'ournet.md', + 'outgun.com', + 'outlawspam.com', + 'outlook.at', + 'outlook.be', + 'outlook.cl', + 'outlook.co.id', + 'outlook.co.il', + 'outlook.co.nz', + 'outlook.co.th', + 'outlook.com', + 'outlook.com.au', + 'outlook.com.br', + 'outlook.com.gr', + 'outlook.com.pe', + 'outlook.com.tr', + 'outlook.com.vn', + 'outlook.cz', + 'outlook.de', + 'outlook.dk', + 'outlook.es', + 'outlook.fr', + 'outlook.hu', + 'outlook.ie', + 'outlook.in', + 'outlook.it', + 'outlook.jp', + 'outlook.kr', + 'outlook.lv', + 'outlook.my', + 'outlook.nl', + 'outlook.ph', + 'outlook.pt', + 'outlook.sa', + 'outlook.sg', + 'outlook.sk', + 'over-the-rainbow.com', + 'ownmail.net', + 'ozbytes.net.au', + 'ozemail.com.au', + 'pacbell.net', + 'pacific-ocean.com', + 'pacific-re.com', + 'pacificwest.com', + 'packersfan.com', + 'pagina.de', + 'pagons.org', + 'pakistanmail.com', + 'pakistanoye.com', + 'palestinemail.com', + 'pandora.be', + 'papierkorb.me', + 'parkjiyoon.com', + 'parsmail.com', + 'partlycloudy.com', + 'partybombe.de', + 'partyheld.de', + 'partynight.at', + 'parvazi.com', + 'passwordmail.com', + 'pathfindermail.com', + 'pconnections.net', + 'pcpostal.com', + 'pcsrock.com', + 'pcusers.otherinbox.com', + 'pediatrician.com', + 'penpen.com', + 'peoplepc.com', + 'peopleweb.com', + 'pepbot.com', + 'perfectmail.com', + 'perso.be', + 'personal.ro', + 'personales.com', + 'petlover.com', + 'petml.com', + 'pettypool.com', + 'pezeshkpour.com', + 'pfui.ru', + 'phayze.com', + 'phone.net', + 'photo-impact.eu', + 'photographer.net', + 'phpbb.uu.gl', + 'phreaker.net', + 'phus8kajuspa.cu.cc', + 'physicist.net', + 'pianomail.com', + 'pickupman.com', + 'picusnet.com', + 'pigpig.net', + 'pinoymail.com', + 'piracha.net', + 'pisem.net', + 'pjjkp.com', + 'planet.nl', + 'planetaccess.com', + 'planetarymotion.net', + 'planetearthinter.net', + 'planetmail.com', + 'planetmail.net', + 'planetout.com', + 'plasa.com', + 'playersodds.com', + 'playful.com', + 'playstation.sony.com', + 'plus.com', + 'plus.google.com', + 'plusmail.com.br', + 'pm.me', + 'pmail.net', + 'pobox.hu', + 'pobox.sk', + 'pochta.ru', + 'poczta.fm', + 'poczta.onet.pl', + 'poetic.com', + 'pokemail.net', + 'pokemonpost.com', + 'pokepost.com', + 'polandmail.com', + 'polbox.com', + 'policeoffice.com', + 'politician.com', + 'polizisten-duzer.de', + 'polyfaust.com', + 'pool-sharks.com', + 'poond.com', + 'popaccount.com', + 'popmail.com', + 'popsmail.com', + 'popstar.com', + 'portugalmail.com', + 'portugalmail.pt', + 'portugalnet.com', + 'positive-thinking.com', + 'post.com', + 'post.cz', + 'post.sk', + 'posta.ro', + 'postaccesslite.com', + 'postafree.com', + 'postaweb.com', + 'posteo.at', + 'posteo.be', + 'posteo.ch', + 'posteo.cl', + 'posteo.co', + 'posteo.de', + 'posteo.dk', + 'posteo.es', + 'posteo.gl', + 'posteo.net', + 'posteo.no', + 'posteo.us', + 'postfach.cc', + 'postinbox.com', + 'postino.ch', + 'postmark.net', + 'postmaster.co.uk', + 'postmaster.twitter.com', + 'postpro.net', + 'pousa.com', + 'powerfan.com', + 'pp.inet.fi', + 'praize.com', + 'premium-mail.fr', + 'premiumservice.com', + 'presidency.com', + 'press.co.jp', + 'priest.com', + 'primposta.com', + 'primposta.hu', + 'privy-mail.com', + 'privymail.de', + 'pro.hu', + 'probemail.com', + 'prodigy.net', + 'progetplus.it', + 'programist.ru', + 'programmer.net', + 'programozo.hu', + 'proinbox.com', + 'project2k.com', + 'promessage.com', + 'prontomail.com', + 'protestant.com', + 'proton.me', + 'protonmail.ch', + 'protonmail.com', + 'prydirect.info', + 'psv-supporter.com', + 'ptd.net', + 'public-files.de', + 'public.usa.com', + 'publicist.com', + 'pulp-fiction.com', + 'punkass.com', + 'purpleturtle.com', + 'put2.net', + 'pwrby.com', + 'q.com', + 'qatarmail.com', + 'qmail.com', + 'qprfans.com', + 'qq.com', + 'qrio.com', + 'quackquack.com', + 'quakemail.com', + 'qualityservice.com', + 'quantentunnel.de', + 'qudsmail.com', + 'quepasa.com', + 'quickhosts.com', + 'quickmail.nl', + 'quicknet.nl', + 'quickwebmail.com', + 'quiklinks.com', + 'quikmail.com', + 'qv7.info', + 'qwest.net', + 'qwestoffice.net', + 'r-o-o-t.com', + 'raakim.com', + 'racedriver.com', + 'racefanz.com', + 'racingfan.com.au', + 'racingmail.com', + 'radicalz.com', + 'radiku.ye.vc', + 'radiologist.net', + 'ragingbull.com', + 'ralib.com', + 'rambler.ru', + 'ranmamail.com', + 'rastogi.net', + 'ratt-n-roll.com', + 'rattle-snake.com', + 'raubtierbaendiger.de', + 'ravearena.com', + 'ravemail.com', + 'razormail.com', + 'rccgmail.org', + 'rcn.com', + 'realemail.net', + 'reality-concept.club', + 'reallyfast.biz', + 'reallyfast.info', + 'reallymymail.com', + 'realradiomail.com', + 'realtyagent.com', + 'reborn.com', + 'reconmail.com', + 'recycler.com', + 'recyclermail.com', + 'rediff.com', + 'rediffmail.com', + 'rediffmailpro.com', + 'rednecks.com', + 'redseven.de', + 'redsfans.com', + 'regbypass.com', + 'reggaefan.com', + 'registerednurses.com', + 'regspaces.tk', + 'reincarnate.com', + 'religious.com', + 'remail.ga', + 'renren.com', + 'repairman.com', + 'reply.hu', + 'reply.ticketmaster.com', + 'representative.com', + 'rescueteam.com', + 'resgedvgfed.tk', + 'resource.calendar.google.com', + 'resumemail.com', + 'rezai.com', + 'rhyta.com', + 'richmondhill.com', + 'rickymail.com', + 'rin.ru', + 'riopreto.com.br', + 'rklips.com', + 'rn.com', + 'ro.ru', + 'roadrunner.com', + 'roanokemail.com', + 'rock.com', + 'rocketmail.com', + 'rocketship.com', + 'rockfan.com', + 'rodrun.com', + 'rogers.com', + 'rome.com', + 'roosh.com', + 'rootprompt.org', + 'roughnet.com', + 'royal.net', + 'rr.com', + 'rrohio.com', + 'rsub.com', + 'rubyridge.com', + 'runbox.com', + 'rushpost.com', + 'ruttolibero.com', + 'rvshop.com', + 's-mail.com', + 'sabreshockey.com', + 'sacbeemail.com', + 'saeuferleber.de', + 'safe-mail.net', + 'safrica.com', + 'sagra.lu', + 'sags-per-mail.de', + 'sailormoon.com', + 'saintly.com', + 'saintmail.net', + 'sale-sale-sale.com', + 'salehi.net', + 'salesperson.net', + 'samerica.com', + 'samilan.net', + 'sammimail.com', + 'sandelf.de', + 'sanfranmail.com', + 'sanook.com', + 'sapo.pt', + 'saudia.com', + 'savelife.ml', + 'sayhi.net', + 'saynotospams.com', + 'sbcglbal.net', + 'sbcglobal.com', + 'sbcglobal.net', + 'scandalmail.com', + 'scarlet.nl', + 'schafmail.de', + 'schizo.com', + 'schmusemail.de', + 'schoolemail.com', + 'schoolmail.com', + 'schoolsucks.com', + 'schreib-doch-mal-wieder.de', + 'schweiz.org', + 'sci.fi', + 'scientist.com', + 'scifianime.com', + 'scotland.com', + 'scotlandmail.com', + 'scottishmail.co.uk', + 'scottsboro.org', + 'scubadiving.com', + 'seanet.com', + 'search.ua', + 'searchwales.com', + 'sebil.com', + 'seckinmail.com', + 'secret-police.com', + 'secretary.net', + 'secretservices.net', + 'secure-mail.biz', + 'secure-mail.cc', + 'seductive.com', + 'seekstoyboy.com', + 'seguros.com.br', + 'selfdestructingmail.com', + 'send.hu', + 'sendme.cz', + 'sendspamhere.com', + 'sent.as', + 'sent.at', + 'sent.com', + 'sentrismail.com', + 'serga.com.ar', + 'servemymail.com', + 'servermaps.net', + 'sesmail.com', + 'sexmagnet.com', + 'seznam.cz', + 'shahweb.net', + 'shaniastuff.com', + 'shared-files.de', + 'sharedmailbox.org', + 'sharklasers.com', + 'sharmaweb.com', + 'shaw.ca', + 'she.com', + 'shieldedmail.com', + 'shinedyoureyes.com', + 'shitaway.cf', + 'shitaway.cu.cc', + 'shitaway.ga', + 'shitaway.gq', + 'shitaway.ml', + 'shitaway.tk', + 'shitaway.usa.cc', + 'shitmail.de', + 'shitmail.org', + 'shitware.nl', + 'shockinmytown.cu.cc', + 'shootmail.com', + 'shortmail.com', + 'shotgun.hu', + 'showslow.de', + 'shuf.com', + 'sialkotcity.com', + 'sialkotian.com', + 'sialkotoye.com', + 'sify.com', + 'silkroad.net', + 'sina.cn', + 'sina.com', + 'sinamail.com', + 'singapore.com', + 'singles4jesus.com', + 'singmail.com', + 'singnet.com.sg', + 'sinnlos-mail.de', + 'siteposter.net', + 'skafan.com', + 'skeefmail.com', + 'skim.com', + 'skizo.hu', + 'skrx.tk', + 'sky.com', + 'skynet.be', + 'slamdunkfan.com', + 'slave-auctions.net', + 'slingshot.com', + 'slippery.email', + 'slipry.net', + 'slo.net', + 'slotter.com', + 'smap.4nmv.ru', + 'smapxsmap.net', + 'smashmail.de', + 'smellrear.com', + 'smileyface.comsmithemail.net', + 'smoothmail.com', + 'sms.at', + 'snail-mail.net', + 'snakebite.com', + 'snakemail.com', + 'sndt.net', + 'sneakemail.com', + 'snet.net', + 'sniper.hu', + 'snkmail.com', + 'snoopymail.com', + 'snowboarding.com', + 'snowdonia.net', + 'socamail.com', + 'socceramerica.net', + 'soccermail.com', + 'soccermomz.com', + 'social-mailer.tk', + 'socialworker.net', + 'sociologist.com', + 'sofort-mail.de', + 'sofortmail.de', + 'softhome.net', + 'sogou.com', + 'sohu.com', + 'sol.dk', + 'solar-impact.pro', + 'solcon.nl', + 'soldier.hu', + 'solution4u.com', + 'solvemail.info', + 'songwriter.net', + 'sonnenkinder.org', + 'soodomail.com', + 'soon.com', + 'soulfoodcookbook.com', + 'sp.nl', + 'space-bank.com', + 'space-man.com', + 'space-ship.com', + 'space-travel.com', + 'space.com', + 'spacemart.com', + 'spacetowns.com', + 'spacewar.com', + 'spainmail.com', + 'spam.2012-2016.ru', + 'spam.care', + 'spamavert.com', + 'spambob.com', + 'spambob.org', + 'spambog.net', + 'spambooger.com', + 'spambox.xyz', + 'spamcero.com', + 'spamdecoy.net', + 'spameater.com', + 'spameater.org', + 'spamex.com', + 'spamfree24.info', + 'spamfree24.net', + 'spamgoes.in', + 'spaminator.de', + 'spamkill.info', + 'spaml.com', + 'spamoff.de', + 'spamstack.net', + 'spartapiet.com', + 'spazmail.com', + 'speedemail.net', + 'speedpost.net', + 'speedrules.com', + 'speedrulz.com', + 'speedymail.org', + 'sperke.net', + 'spils.com', + 'spinfinder.com', + 'spl.at', + 'spoko.pl', + 'spoofmail.de', + 'sportemail.com', + 'sportsmail.com', + 'sporttruckdriver.com', + 'spray.no', + 'spray.se', + 'spybox.de', + 'spymac.com', + 'sraka.xyz', + 'srilankan.net', + 'ssl-mail.com', + 'st-davids.net', + 'stade.fr', + 'stalag13.com', + 'stargateradio.com', + 'starmail.com', + 'starmail.org', + 'starmedia.com', + 'starplace.com', + 'starspath.com', + 'start.com.au', + 'startkeys.com', + 'stinkefinger.net', + 'stipte.nl', + 'stoned.com', + 'stones.com', + 'stop-my-spam.pp.ua', + 'stopdropandroll.com', + 'storksite.com', + 'streber24.de', + 'streetwisemail.com', + 'stribmail.com', + 'strompost.com', + 'strongguy.com', + 'student.su', + 'studentcenter.org', + 'stuffmail.de', + 'subram.com', + 'sudanmail.net', + 'sudolife.me', + 'sudolife.net', + 'sudomail.biz', + 'sudomail.com', + 'sudomail.net', + 'sudoverse.com', + 'sudoverse.net', + 'sudoweb.net', + 'sudoworld.com', + 'sudoworld.net', + 'suhabi.com', + 'suisse.org', + 'sukhumvit.net', + 'sunpoint.net', + 'sunrise-sunset.com', + 'sunsgame.com', + 'sunumail.sn', + 'suomi24.fi', + 'superdada.com', + 'supereva.it', + 'supermail.ru', + 'superrito.com', + 'superstachel.de', + 'surat.com', + 'surf3.net', + 'surfree.com', + 'surfy.net', + 'surgical.net', + 'surimail.com', + 'survivormail.com', + 'susi.ml', + 'svk.jp', + 'swbell.net', + 'sweb.cz', + 'swedenmail.com', + 'sweetville.net', + 'sweetxxx.de', + 'swift-mail.com', + 'swiftdesk.com', + 'swingeasyhithard.com', + 'swingfan.com', + 'swipermail.zzn.com', + 'swirve.com', + 'swissinfo.org', + 'swissmail.com', + 'swissmail.net', + 'switchboardmail.com', + 'switzerland.org', + 'sx172.com', + 'syom.com', + 'syriamail.com', + 't-online.de', + 't.psh.me', + 't2mail.com', + 'tafmail.com', + 'takuyakimura.com', + 'talk21.com', + 'talkcity.com', + 'talkinator.com', + 'tamil.com', + 'tampabay.rr.com', + 'tankpolice.com', + 'tatanova.com', + 'tbwt.com', + 'tcc.on.ca', + 'tds.net', + 'teachermail.net', + 'teachers.org', + 'teamdiscovery.com', + 'teamtulsa.net', + 'tech-center.com', + 'tech4peace.org', + 'techemail.com', + 'techie.com', + 'technisamail.co.za', + 'technologist.com', + 'techscout.com', + 'techspot.com', + 'teenagedirtbag.com', + 'tele2.nl', + 'telebot.com', + 'telefonica.net', + 'teleline.es', + 'telenet.be', + 'telepac.pt', + 'telerymd.com', + 'teleworm.us', + 'telfort.nl', + 'telfortglasvezel.nl', + 'telinco.net', + 'telkom.net', + 'telpage.net', + 'telstra.com', + 'telstra.com.au', + 'temp-mail.com', + 'temp-mail.de', + 'temp-mail.org', + 'temp.headstrong.de', + 'tempail.com', + 'tempemail.biz', + 'tempmail.net', + 'tempmail.us', + 'tempmail2.com', + 'tempmaildemo.com', + 'tempmailer.com', + 'temporarioemail.com.br', + 'temporaryemail.us', + 'tempthe.net', + 'tempymail.com', + 'temtulsa.net', + 'tenchiclub.com', + 'tenderkiss.com', + 'tennismail.com', + 'terminverpennt.de', + 'terra.cl', + 'terra.com', + 'terra.com.ar', + 'terra.com.br', + 'terra.es', + 'test.com', + 'test.de', + 'tfanus.com.er', + 'tfz.net', + 'thai.com', + 'thaimail.com', + 'thaimail.net', + 'thanksnospam.info', + 'the-african.com', + 'the-airforce.com', + 'the-aliens.com', + 'the-american.com', + 'the-animal.com', + 'the-army.com', + 'the-astronaut.com', + 'the-beauty.com', + 'the-big-apple.com', + 'the-biker.com', + 'the-boss.com', + 'the-brazilian.com', + 'the-canadian.com', + 'the-canuck.com', + 'the-captain.com', + 'the-chinese.com', + 'the-country.com', + 'the-cowboy.com', + 'the-davis-home.com', + 'the-dutchman.com', + 'the-eagles.com', + 'the-englishman.com', + 'the-fastest.net', + 'the-fool.com', + 'the-frenchman.com', + 'the-galaxy.net', + 'the-genius.com', + 'the-gentleman.com', + 'the-german.com', + 'the-gremlin.com', + 'the-hooligan.com', + 'the-italian.com', + 'the-japanese.com', + 'the-lair.com', + 'the-madman.com', + 'the-mailinglist.com', + 'the-marine.com', + 'the-master.com', + 'the-mexican.com', + 'the-ministry.com', + 'the-monkey.com', + 'the-newsletter.net', + 'the-pentagon.com', + 'the-police.com', + 'the-prayer.com', + 'the-professional.com', + 'the-quickest.com', + 'the-russian.com', + 'the-snake.com', + 'the-spaceman.com', + 'the-stock-market.com', + 'the-student.net', + 'the-whitehouse.net', + 'the-wild-west.com', + 'the18th.com', + 'thecoolguy.com', + 'thecriminals.com', + 'thedoghousemail.com', + 'thedorm.com', + 'theend.hu', + 'theglobe.com', + 'thegolfcourse.com', + 'thegooner.com', + 'theheadoffice.com', + 'theinternetemail.com', + 'thelanddownunder.com', + 'themail.com', + 'themillionare.net', + 'theoffice.net', + 'theplate.com', + 'thepokerface.com', + 'thepostmaster.net', + 'theraces.com', + 'theracetrack.com', + 'therapist.net', + 'thestreetfighter.com', + 'theteebox.com', + 'thewatercooler.com', + 'thewebpros.co.uk', + 'thewizzard.com', + 'thewizzkid.com', + 'thezhangs.net', + 'thirdage.com', + 'thisgirl.com', + 'thraml.com', + 'throwam.com', + 'thundermail.com', + 'tidni.com', + 'timein.net', + 'tiscali.at', + 'tiscali.be', + 'tiscali.co.uk', + 'tiscali.it', + 'tiscali.lu', + 'tiscali.se', + 'tkcity.com', + 'tmail.ws', + 'toast.com', + 'toke.com', + 'tom.com', + 'toolsource.com', + 'toomail.biz', + 'toothfairy.com', + 'topchat.com', + 'topgamers.co.uk', + 'topletter.com', + 'topmail-files.de', + 'topmail.com.ar', + 'topsurf.com', + 'torchmail.com', + 'torontomail.com', + 'tortenboxer.de', + 'totalmail.de', + 'totalmusic.net', + 'townisp.com', + 'tpg.com.au', + 'trash-amil.com', + 'trash-mail.ga', + 'trash-mail.ml', + 'trash2010.com', + 'trash2011.com', + 'trashdevil.de', + 'trashymail.net', + 'travel.li', + 'trayna.com', + 'trialbytrivia.com', + 'trickmail.net', + 'trimix.cn', + 'tritium.net', + 'trmailbox.com', + 'tropicalstorm.com', + 'truckerz.com', + 'truckracer.com', + 'truckracers.com', + 'trust-me.com', + 'truthmail.com', + 'tsamail.co.za', + 'ttml.co.in', + 'tunisiamail.com', + 'turboprinz.de', + 'turboprinzessin.de', + 'turkey.com', + 'turual.com', + 'tut.by', + 'tvstar.com', + 'twc.com', + 'twcny.com', + 'twinstarsmail.com', + 'tx.rr.com', + 'tycoonmail.com', + 'typemail.com', + 'u14269.ml', + 'u2club.com', + 'ua.fm', + 'uae.ac', + 'uaemail.com', + 'ubbi.com', + 'ubbi.com.br', + 'uboot.com', + 'uk2.net', + 'uk2k.com', + 'uk2net.com', + 'uk7.net', + 'uk8.net', + 'ukbuilder.com', + 'ukcool.com', + 'ukdreamcast.com', + 'ukmail.org', + 'ukmax.com', + 'ukr.net', + 'uku.co.uk', + 'ultapulta.com', + 'ultra.fyi', + 'ultrapostman.com', + 'ummah.org', + 'umpire.com', + 'unbounded.com', + 'unforgettable.com', + 'uni.de', + 'unican.es', + 'unihome.com', + 'unitybox.de', + 'universal.pt', + 'uno.ee', + 'uno.it', + 'unofree.it', + 'unterderbruecke.de', + 'uol.com.ar', + 'uol.com.br', + 'uol.com.co', + 'uol.com.mx', + 'uol.com.ve', + 'uole.com', + 'uole.com.ve', + 'uolmail.com', + 'uomail.com', + 'upc.nl', + 'upcmail.nl', + 'upf.org', + 'uplipht.com', + 'ureach.com', + 'urgentmail.biz', + 'urhen.com', + 'uroid.com', + 'usa.com', + 'usa.net', + 'usaaccess.net', + 'usanetmail.com', + 'used-product.fr', + 'usermail.com', + 'username.e4ward.com', + 'usma.net', + 'usmc.net', + 'uswestmail.net', + 'uymail.com', + 'uyuyuy.com', + 'v-sexi.com', + 'vaasfc4.tk', + 'vahoo.com', + 'valemail.net', + 'vampirehunter.com', + 'varbizmail.com', + 'vcmail.com', + 'velnet.co.uk', + 'velocall.com', + 'verizon.net', + 'verizonmail.com', + 'verlass-mich-nicht.de', + 'versatel.nl', + 'veryfast.biz', + 'veryrealemail.com', + 'veryspeedy.net', + 'vfemail.net', + 'vickaentb.tk', + 'videotron.ca', + 'viditag.com', + 'viewcastmedia.com', + 'viewcastmedia.net', + 'vinbazar.com', + 'violinmakers.co.uk', + 'vip.126.com', + 'vip.21cn.com', + 'vip.citiz.net', + 'vip.gr', + 'vip.onet.pl', + 'vip.qq.com', + 'vip.sina.com', + 'vipmail.ru', + 'virgilio.it', + 'virgin.net', + 'virginbroadband.com.au', + 'virginmedia.com', + 'virtualmail.com', + 'visitmail.com', + 'visitweb.com', + 'visto.com', + 'visualcities.com', + 'vivavelocity.com', + 'vivianhsu.net', + 'vjtimail.com', + 'vkcode.ru', + 'vnet.citiz.net', + 'vnn.vn', + 'vodafone.nl', + 'vodafonethuis.nl', + 'volcanomail.com', + 'vollbio.de', + 'volloeko.de', + 'vomoto.com', + 'vorsicht-bissig.de', + 'vorsicht-scharf.de', + 'vote-democrats.com', + 'vote-hillary.com', + 'vote-republicans.com', + 'vote4gop.org', + 'votenet.com', + 'vp.pl', + 'vr9.com', + 'vubby.com', + 'w3.to', + 'wahoye.com', + 'walala.org', + 'wales2000.net', + 'walkmail.net', + 'walkmail.ru', + 'walla.co.il', + 'walla.com', + 'wam.co.za', + 'wanadoo.es', + 'wanadoo.fr', + 'war-im-urlaub.de', + 'warmmail.com', + 'warpmail.net', + 'warrior.hu', + 'waumail.com', + 'wazabi.club', + 'wbdet.com', + 'wearab.net', + 'web-contact.info', + 'web-emailbox.eu', + 'web-ideal.fr', + 'web-mail.com.ar', + 'web-mail.pp.ua', + 'web-police.com', + 'web.de', + 'webave.com', + 'webcammail.com', + 'webcity.ca', + 'webcontact-france.eu', + 'webdream.com', + 'webindia123.com', + 'webjump.com', + 'webm4il.info', + 'webmail.co.yu', + 'webmail.co.za', + 'webmail.hu', + 'webmails.com', + 'webname.com', + 'webprogramming.com', + 'webstation.com', + 'websurfer.co.za', + 'webtopmail.com', + 'webuser.in', + 'wee.my', + 'weedmail.com', + 'weekmail.com', + 'weekonline.com', + 'wefjo.grn.cc', + 'weg-werf-email.de', + 'wegas.ru', + 'wegwerf-emails.de', + 'wegwerfmail.info', + 'wegwerpmailadres.nl', + 'wehshee.com', + 'weibsvolk.de', + 'weibsvolk.org', + 'weinenvorglueck.de', + 'welsh-lady.com', + 'westnet.com', + 'westnet.com.au', + 'wetrainbayarea.com', + 'wfgdfhj.tk', + 'whale-mail.com', + 'whartontx.com', + 'whatiaas.com', + 'whatpaas.com', + 'wheelweb.com', + 'whipmail.com', + 'whoever.com', + 'whoopymail.com', + 'whtjddn.33mail.com', + 'wi.rr.com', + 'wi.twcbc.com', + 'wickmail.net', + 'wideopenwest.com', + 'wildmail.com', + 'wilemail.com', + 'will-hier-weg.de', + 'windowslive.com', + 'windrivers.net', + 'windstream.net', + 'wingnutz.com', + 'winmail.com.au', + 'winning.com', + 'wir-haben-nachwuchs.de', + 'wir-sind-cool.org', + 'wirsindcool.de', + 'witty.com', + 'wiz.cc', + 'wkbwmail.com', + 'wmail.cf', + 'wo.com.cn', + 'woh.rr.com', + 'wolf-web.com', + 'wolke7.net', + 'wollan.info', + 'wombles.com', + 'women-at-work.org', + 'wongfaye.com', + 'wooow.it', + 'worker.com', + 'workmail.com', + 'worldemail.com', + 'worldnet.att.net', + 'wormseo.cn', + 'wosaddict.com', + 'wouldilie.com', + 'wovz.cu.cc', + 'wowgirl.com', + 'wowmail.com', + 'wowway.com', + 'wp.pl', + 'wptamail.com', + 'wrexham.net', + 'writeme.com', + 'writemeback.com', + 'wrongmail.com', + 'wtvhmail.com', + 'wwdg.com', + 'www.com', + 'www.e4ward.com', + 'www2000.net', + 'wx88.net', + 'wxs.net', + 'x-mail.net', + 'x-networks.net', + 'x5g.com', + 'xagloo.com', + 'xaker.ru', + 'xing886.uu.gl', + 'xmastime.com', + 'xms.nl', + 'xmsg.com', + 'xoom.com', + 'xoxox.cc', + 'xpressmail.zzn.com', + 'xs4all.nl', + 'xsecurity.org', + 'xsmail.com', + 'xtra.co.nz', + 'xuno.com', + 'xww.ro', + 'xy9ce.tk', + 'xyzfree.net', + 'xzapmail.com', + 'y7mail.com', + 'ya.ru', + 'yada-yada.com', + 'yaho.com', + 'yahoo.ae', + 'yahoo.at', + 'yahoo.be', + 'yahoo.ca', + 'yahoo.ch', + 'yahoo.cn', + 'yahoo.co', + 'yahoo.co.id', + 'yahoo.co.il', + 'yahoo.co.in', + 'yahoo.co.jp', + 'yahoo.co.kr', + 'yahoo.co.nz', + 'yahoo.co.th', + 'yahoo.co.uk', + 'yahoo.co.za', + 'yahoo.com', + 'yahoo.com.ar', + 'yahoo.com.au', + 'yahoo.com.br', + 'yahoo.com.cn', + 'yahoo.com.co', + 'yahoo.com.hk', + 'yahoo.com.is', + 'yahoo.com.mx', + 'yahoo.com.my', + 'yahoo.com.ph', + 'yahoo.com.ru', + 'yahoo.com.sg', + 'yahoo.com.tr', + 'yahoo.com.tw', + 'yahoo.com.vn', + 'yahoo.cz', + 'yahoo.de', + 'yahoo.dk', + 'yahoo.es', + 'yahoo.fi', + 'yahoo.fr', + 'yahoo.gr', + 'yahoo.hu', + 'yahoo.ie', + 'yahoo.in', + 'yahoo.it', + 'yahoo.jp', + 'yahoo.nl', + 'yahoo.no', + 'yahoo.pl', + 'yahoo.pt', + 'yahoo.ro', + 'yahoo.ru', + 'yahoo.se', + 'yahoofs.com', + 'yalla.com', + 'yalla.com.lb', + 'yalook.com', + 'yam.com', + 'yandex.com', + 'yandex.pl', + 'yandex.ru', + 'yandex.ua', + 'yapost.com', + 'yapped.net', + 'yawmail.com', + 'yeah.net', + 'yebox.com', + 'yehey.com', + 'yemenmail.com', + 'yepmail.net', + 'yert.ye.vc', + 'yesey.net', + 'yifan.net', + 'ymail.com', + 'yogotemail.com', + 'yomail.info', + 'yopmail.com', + 'yopmail.pp.ua', + 'yopolis.com', + 'yopweb.com', + 'youareadork.com', + 'youmailr.com', + 'your-house.com', + 'your-mail.com', + 'yourinbox.com', + 'yourlifesucks.cu.cc', + 'yourlover.net', + 'yourname.freeservers.com', + 'yournightmare.com', + 'yours.com', + 'yourssincerely.com', + 'yoursubdomain.zzn.com', + 'yourteacher.net', + 'yourwap.com', + 'yuuhuu.net', + 'yyhmail.com', + 'z1p.biz', + 'zahadum.com', + 'zaktouni.fr', + 'zeepost.nl', + 'zetmail.com', + 'zhaowei.net', + 'zhouemail.510520.org', + 'ziggo.nl', + 'zionweb.org', + 'zip.net', + 'zipido.com', + 'ziplip.com', + 'zipmail.com', + 'zipmail.com.br', + 'zipmax.com', + 'zmail.ru', + 'zoemail.com', + 'zoemail.org', + 'zoho.com', + 'zohomail.com', + 'zomg.info', + 'zonnet.nl', + 'zoominternet.net', + 'zubee.com', + 'zuvio.com', + 'zuzzurello.com', + 'zwallet.com', + 'zweb.in', + 'zxcv.com', + 'zxcvbnm.com', + 'zybermail.com', + 'zydecofan.com', + 'zzn.com', + 'zzom.co.uk', + 'zzz.com', + 'zzz.pl', ]; diff --git a/data/sources.php b/data/sources.php index 8b24bba..3665cd4 100644 --- a/data/sources.php +++ b/data/sources.php @@ -56,6 +56,12 @@ 'enabled' => true, 'description' => 'Manually managed free email domains', 'configFile' => 'free-domains-manual.php' + ], + 'kikobeats' => [ + 'name' => 'Kikobeats Free Email Domains', + 'url' => 'https://raw.githubusercontent.com/Kikobeats/free-email-domains/master/domains.json', + 'enabled' => true, + 'description' => 'Free email domains from Kikobeats repository' ] ] ]; diff --git a/import.php b/import.php index 94a6dc7..860ef49 100644 --- a/import.php +++ b/import.php @@ -53,6 +53,11 @@ 'name' => 'Manual Free Email Domains', 'url' => null, 'configFile' => CONFIG_DIR . '/free-domains-manual.php' + ], + 'kikobeats' => [ + 'name' => 'Kikobeats Free Email Domains', + 'url' => 'https://raw.githubusercontent.com/Kikobeats/free-email-domains/master/domains.json', + 'configFile' => CONFIG_DIR . '/free-domains-kikobeats.php' ] ]; @@ -290,6 +295,8 @@ function fetchSource(string $sourceKey, array $sourceConfig, string $type): arra switch ($sourceKey) { case 'manual': return loadManualFreeDomains($sourceConfig); + case 'kikobeats': + return fetchKikobeatsDomains($sourceConfig); default: throw new \Exception("Unknown free source: {$sourceKey}"); } @@ -552,6 +559,63 @@ function loadManualDisposableDomains(array $sourceConfig): array return $domains; } +/** + * Fetch domains from Kikobeats repository + */ +function fetchKikobeatsDomains(array $sourceConfig): array +{ + try { + $client = new \Utopia\Fetch\Client(); + + $response = $client->fetch( + url: $sourceConfig['url'], + method: \Utopia\Fetch\Client::METHOD_GET + ); + + if ($response->getStatusCode() !== 200) { + throw new \Exception('HTTP ' . $response->getStatusCode()); + } + + $content = $response->getBody(); + + } catch (\Exception $e) { + throw new \Exception('Network error: ' . $e->getMessage()); + } + + $domains = []; + $processed = 0; + $valid = 0; + + // Parse JSON content + $jsonData = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception('Invalid JSON response: ' . json_last_error_msg()); + } + + if (!is_array($jsonData)) { + throw new \Exception('Expected array in JSON response'); + } + + foreach ($jsonData as $domain) { + $domain = trim($domain); + $processed++; + + if (empty($domain)) { + continue; + } + + if (isValidDomain($domain)) { + $domains[] = strtolower($domain); + $valid++; + } + } + + Console::info(" Processed {$processed} domains, found {$valid} valid domains"); + + return $domains; +} + /** * Load manual free domains */ From 7c0aed9ca89ff024d368765e2e5ee3548e6d4054 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 18 Oct 2025 22:43:57 +0100 Subject: [PATCH 03/21] fixes to format --- data/free-domains-manual.php | 4 +- data/sources.php | 26 ++--- import.php | 177 ++++++++++++++--------------- test-import.php | 133 ---------------------- tests/EmailTest.php | 20 ++-- tests/Validator/EmailLocalTest.php | 1 - 6 files changed, 109 insertions(+), 252 deletions(-) delete mode 100644 test-import.php diff --git a/data/free-domains-manual.php b/data/free-domains-manual.php index b747708..0aa81b4 100644 --- a/data/free-domains-manual.php +++ b/data/free-domains-manual.php @@ -2,10 +2,10 @@ /** * Manual Free Email Domains Configuration - * + * * This file contains manually managed free email domains. * You can add or remove domains here as needed. - * + * * Last updated: 2024-01-01 00:00:00 * Source: Manual configuration */ diff --git a/data/sources.php b/data/sources.php index 3665cd4..fe5e715 100644 --- a/data/sources.php +++ b/data/sources.php @@ -2,10 +2,10 @@ /** * Email Domain Sources Configuration - * + * * This file defines the sources for both disposable and free email domains. * You can modify this file to add, remove, or update sources. - * + * * Last updated: 2024-01-01 00:00:00 */ @@ -16,38 +16,38 @@ 'url' => null, 'enabled' => true, 'description' => 'Manually managed disposable email domains', - 'configFile' => 'disposable-domains-manual.php' + 'configFile' => 'disposable-domains-manual.php', ], 'martenson' => [ 'name' => 'Martenson Disposable Email Domains', 'url' => 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf', 'enabled' => true, - 'description' => 'Comprehensive list of disposable email domains from Martenson repository' + 'description' => 'Comprehensive list of disposable email domains from Martenson repository', ], 'disposable' => [ 'name' => 'Disposable Email Domains', 'url' => 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt', 'enabled' => true, - 'description' => 'Popular disposable email domains list' + 'description' => 'Popular disposable email domains list', ], 'wesbos' => [ 'name' => 'Wes Bos Burner Email Providers', 'url' => 'https://raw.githubusercontent.com/wesbos/burner-email-providers/refs/heads/master/emails.txt', 'enabled' => true, - 'description' => 'Burner email providers from Wes Bos repository' + 'description' => 'Burner email providers from Wes Bos repository', ], 'fakefilter' => [ 'name' => '7c FakeFilter Domains', 'url' => 'https://raw.githubusercontent.com/7c/fakefilter/main/txt/data.txt', 'enabled' => true, - 'description' => 'Fake email domains from 7c FakeFilter' + 'description' => 'Fake email domains from 7c FakeFilter', ], 'adamloving' => [ 'name' => 'Adam Loving Temporary Email Domains', 'url' => 'https://gist.githubusercontent.com/adamloving/4401361/raw/e81212c3caecb54b87ced6392e0a0de2b6466287/temporary-email-address-domains', 'enabled' => true, - 'description' => 'Temporary email domains from Adam Loving gist' - ] + 'description' => 'Temporary email domains from Adam Loving gist', + ], ], 'free' => [ 'manual' => [ @@ -55,13 +55,13 @@ 'url' => null, 'enabled' => true, 'description' => 'Manually managed free email domains', - 'configFile' => 'free-domains-manual.php' + 'configFile' => 'free-domains-manual.php', ], 'kikobeats' => [ 'name' => 'Kikobeats Free Email Domains', 'url' => 'https://raw.githubusercontent.com/Kikobeats/free-email-domains/master/domains.json', 'enabled' => true, - 'description' => 'Free email domains from Kikobeats repository' - ] - ] + 'description' => 'Free email domains from Kikobeats repository', + ], + ], ]; diff --git a/import.php b/import.php index 860ef49..d558c83 100644 --- a/import.php +++ b/import.php @@ -1,13 +1,13 @@ [ 'name' => 'Manual Disposable Email Domains', 'url' => null, - 'configFile' => CONFIG_DIR . '/disposable-domains-manual.php' + 'configFile' => CONFIG_DIR.'/disposable-domains-manual.php', ], 'martenson' => [ 'name' => 'Martenson Disposable Email Domains', 'url' => 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf', - 'configFile' => CONFIG_DIR . '/disposable-domains-martenson.php' + 'configFile' => CONFIG_DIR.'/disposable-domains-martenson.php', ], 'disposable' => [ 'name' => 'Disposable Email Domains', 'url' => 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt', - 'configFile' => CONFIG_DIR . '/disposable-domains-disposable.php' + 'configFile' => CONFIG_DIR.'/disposable-domains-disposable.php', ], 'wesbos' => [ 'name' => 'Wes Bos Burner Email Providers', 'url' => 'https://raw.githubusercontent.com/wesbos/burner-email-providers/refs/heads/master/emails.txt', - 'configFile' => CONFIG_DIR . '/disposable-domains-wesbos.php' + 'configFile' => CONFIG_DIR.'/disposable-domains-wesbos.php', ], 'fakefilter' => [ 'name' => '7c FakeFilter Domains', 'url' => 'https://raw.githubusercontent.com/7c/fakefilter/main/txt/data.txt', - 'configFile' => CONFIG_DIR . '/disposable-domains-fakefilter.php' + 'configFile' => CONFIG_DIR.'/disposable-domains-fakefilter.php', ], 'adamloving' => [ 'name' => 'Adam Loving Temporary Email Domains', 'url' => 'https://gist.githubusercontent.com/adamloving/4401361/raw/e81212c3caecb54b87ced6392e0a0de2b6466287/temporary-email-address-domains', - 'configFile' => CONFIG_DIR . '/disposable-domains-adamloving.php' - ] + 'configFile' => CONFIG_DIR.'/disposable-domains-adamloving.php', + ], ]; /** @@ -52,13 +52,13 @@ 'manual' => [ 'name' => 'Manual Free Email Domains', 'url' => null, - 'configFile' => CONFIG_DIR . '/free-domains-manual.php' + 'configFile' => CONFIG_DIR.'/free-domains-manual.php', ], 'kikobeats' => [ 'name' => 'Kikobeats Free Email Domains', 'url' => 'https://raw.githubusercontent.com/Kikobeats/free-email-domains/master/domains.json', - 'configFile' => CONFIG_DIR . '/free-domains-kikobeats.php' - ] + 'configFile' => CONFIG_DIR.'/free-domains-kikobeats.php', + ], ]; /** @@ -86,18 +86,18 @@ function updateDisposableDomains(bool $commit, bool $force, string $source): voi Console::exit(1); } - Console::info('Fetched ' . count($allDomains) . ' disposable email domains from all sources'); + Console::info('Fetched '.count($allDomains).' disposable email domains from all sources'); showDomainStatistics($allDomains); - if (!$force && isConfigUpToDate($currentDomains, $allDomains)) { + if (! $force && isConfigUpToDate($currentDomains, $allDomains)) { Console::success('Disposable email domains are already up to date'); Console::exit(0); } Console::info('Changes detected:'); - Console::info('- Previous domains count: ' . count($currentDomains)); - Console::info('- New domains count: ' . count($allDomains)); + Console::info('- Previous domains count: '.count($currentDomains)); + Console::info('- New domains count: '.count($allDomains)); if ($commit) { saveConfig('disposable-domains.php', $allDomains, 'Disposable Email Domains'); @@ -107,9 +107,8 @@ function updateDisposableDomains(bool $commit, bool $force, string $source): voi Console::info('Preview of changes:'); showPreview($currentDomains, $allDomains); } - } catch (\Throwable $e) { - Console::error('Error updating disposable email domains: ' . $e->getMessage()); + Console::error('Error updating disposable email domains: '.$e->getMessage()); Console::exit(1); } } @@ -139,18 +138,18 @@ function updateFreeDomains(bool $commit, bool $force, string $source): void Console::exit(1); } - Console::info('Fetched ' . count($allDomains) . ' free email domains from all sources'); + Console::info('Fetched '.count($allDomains).' free email domains from all sources'); showDomainStatistics($allDomains); - if (!$force && isConfigUpToDate($currentDomains, $allDomains)) { + if (! $force && isConfigUpToDate($currentDomains, $allDomains)) { Console::success('Free email domains are already up to date'); Console::exit(0); } Console::info('Changes detected:'); - Console::info('- Previous domains count: ' . count($currentDomains)); - Console::info('- New domains count: ' . count($allDomains)); + Console::info('- Previous domains count: '.count($currentDomains)); + Console::info('- New domains count: '.count($allDomains)); if ($commit) { saveConfig('free-domains.php', $allDomains, 'Free Email Domains'); @@ -160,9 +159,8 @@ function updateFreeDomains(bool $commit, bool $force, string $source): void Console::info('Preview of changes:'); showPreview($currentDomains, $allDomains); } - } catch (\Throwable $e) { - Console::error('Error updating free email domains: ' . $e->getMessage()); + Console::error('Error updating free email domains: '.$e->getMessage()); Console::exit(1); } } @@ -178,16 +176,15 @@ function updateAllDomains(bool $commit, bool $force): void try { // Update disposable domains updateDisposableDomains($commit, $force, ''); - + Console::info(''); - + // Update free domains updateFreeDomains($commit, $force, ''); Console::success('Successfully updated all email domains'); - } catch (\Throwable $e) { - Console::error('Error updating all email domains: ' . $e->getMessage()); + Console::error('Error updating all email domains: '.$e->getMessage()); Console::exit(1); } } @@ -204,23 +201,22 @@ function showStats(): void $freeDomains = loadCurrentConfig('free-domains.php'); Console::info('Current Domain Statistics:'); - Console::info('├─ Disposable domains: ' . count($disposableDomains)); - Console::info('└─ Free domains: ' . count($freeDomains)); + Console::info('├─ Disposable domains: '.count($disposableDomains)); + Console::info('└─ Free domains: '.count($freeDomains)); - if (!empty($disposableDomains)) { + if (! empty($disposableDomains)) { Console::info(''); Console::info('Disposable Domains Analysis:'); showDomainStatistics($disposableDomains); } - if (!empty($freeDomains)) { + if (! empty($freeDomains)) { Console::info(''); Console::info('Free Domains Analysis:'); showDomainStatistics($freeDomains); } - } catch (\Throwable $e) { - Console::error('Error showing statistics: ' . $e->getMessage()); + Console::error('Error showing statistics: '.$e->getMessage()); Console::exit(1); } } @@ -250,9 +246,9 @@ function fetchAllSources(array $sources, string $type): array $allDomains[$domain] = true; // Use associative array to avoid duplicates } - Console::info("✓ Fetched " . count($domains) . " domains from {$sourceConfig['name']}"); + Console::info('✓ Fetched '.count($domains)." domains from {$sourceConfig['name']}"); } catch (\Exception $e) { - Console::warning("⚠ Failed to fetch from {$sourceConfig['name']}: " . $e->getMessage()); + Console::warning("⚠ Failed to fetch from {$sourceConfig['name']}: ".$e->getMessage()); // Continue with other sources even if one fails } } @@ -264,7 +260,7 @@ function fetchAllSources(array $sources, string $type): array $duplicatesRemoved = $totalFetched - count($uniqueDomains); Console::info("Total domains fetched: {$totalFetched}"); Console::info("Duplicates removed: {$duplicatesRemoved}"); - Console::info("Total unique domains after merging all sources: " . count($uniqueDomains)); + Console::info('Total unique domains after merging all sources: '.count($uniqueDomains)); return $uniqueDomains; } @@ -319,13 +315,12 @@ function fetchMartensonDomains(array $sourceConfig): array ); if ($response->getStatusCode() !== 200) { - throw new \Exception('HTTP ' . $response->getStatusCode()); + throw new \Exception('HTTP '.$response->getStatusCode()); } $content = $response->getBody(); - } catch (\Exception $e) { - throw new \Exception('Network error: ' . $e->getMessage()); + throw new \Exception('Network error: '.$e->getMessage()); } $domains = []; @@ -366,13 +361,12 @@ function fetchDisposableDomains(array $sourceConfig): array ); if ($response->getStatusCode() !== 200) { - throw new \Exception('HTTP ' . $response->getStatusCode()); + throw new \Exception('HTTP '.$response->getStatusCode()); } $content = $response->getBody(); - } catch (\Exception $e) { - throw new \Exception('Network error: ' . $e->getMessage()); + throw new \Exception('Network error: '.$e->getMessage()); } $domains = []; @@ -380,7 +374,7 @@ function fetchDisposableDomains(array $sourceConfig): array $valid = 0; $domainList = preg_split('/\s+/', trim($content)); - + foreach ($domainList as $domain) { $domain = trim($domain); $processed++; @@ -414,13 +408,12 @@ function fetchWesbosDomains(array $sourceConfig): array ); if ($response->getStatusCode() !== 200) { - throw new \Exception('HTTP ' . $response->getStatusCode()); + throw new \Exception('HTTP '.$response->getStatusCode()); } $content = $response->getBody(); - } catch (\Exception $e) { - throw new \Exception('Network error: ' . $e->getMessage()); + throw new \Exception('Network error: '.$e->getMessage()); } $domains = []; @@ -428,7 +421,7 @@ function fetchWesbosDomains(array $sourceConfig): array $valid = 0; $domainList = preg_split('/\s+/', trim($content)); - + foreach ($domainList as $domain) { $domain = trim($domain); $processed++; @@ -462,13 +455,12 @@ function fetchFakeFilterDomains(array $sourceConfig): array ); if ($response->getStatusCode() !== 200) { - throw new \Exception('HTTP ' . $response->getStatusCode()); + throw new \Exception('HTTP '.$response->getStatusCode()); } $content = $response->getBody(); - } catch (\Exception $e) { - throw new \Exception('Network error: ' . $e->getMessage()); + throw new \Exception('Network error: '.$e->getMessage()); } $domains = []; @@ -509,13 +501,12 @@ function fetchAdamLovingDomains(array $sourceConfig): array ); if ($response->getStatusCode() !== 200) { - throw new \Exception('HTTP ' . $response->getStatusCode()); + throw new \Exception('HTTP '.$response->getStatusCode()); } $content = $response->getBody(); - } catch (\Exception $e) { - throw new \Exception('Network error: ' . $e->getMessage()); + throw new \Exception('Network error: '.$e->getMessage()); } $domains = []; @@ -523,7 +514,7 @@ function fetchAdamLovingDomains(array $sourceConfig): array $valid = 0; $domainList = preg_split('/\s+/', trim($content)); - + foreach ($domainList as $domain) { $domain = trim($domain); $processed++; @@ -548,13 +539,14 @@ function fetchAdamLovingDomains(array $sourceConfig): array */ function loadManualDisposableDomains(array $sourceConfig): array { - if (!file_exists($sourceConfig['configFile'])) { - Console::info(" Manual config file not found, creating empty list"); + if (! file_exists($sourceConfig['configFile'])) { + Console::info(' Manual config file not found, creating empty list'); + return []; } $domains = include $sourceConfig['configFile']; - Console::info(" Loaded " . count($domains) . " domains from manual config"); + Console::info(' Loaded '.count($domains).' domains from manual config'); return $domains; } @@ -573,13 +565,12 @@ function fetchKikobeatsDomains(array $sourceConfig): array ); if ($response->getStatusCode() !== 200) { - throw new \Exception('HTTP ' . $response->getStatusCode()); + throw new \Exception('HTTP '.$response->getStatusCode()); } $content = $response->getBody(); - } catch (\Exception $e) { - throw new \Exception('Network error: ' . $e->getMessage()); + throw new \Exception('Network error: '.$e->getMessage()); } $domains = []; @@ -588,12 +579,12 @@ function fetchKikobeatsDomains(array $sourceConfig): array // Parse JSON content $jsonData = json_decode($content, true); - + if (json_last_error() !== JSON_ERROR_NONE) { - throw new \Exception('Invalid JSON response: ' . json_last_error_msg()); + throw new \Exception('Invalid JSON response: '.json_last_error_msg()); } - if (!is_array($jsonData)) { + if (! is_array($jsonData)) { throw new \Exception('Expected array in JSON response'); } @@ -621,13 +612,14 @@ function fetchKikobeatsDomains(array $sourceConfig): array */ function loadManualFreeDomains(array $sourceConfig): array { - if (!file_exists($sourceConfig['configFile'])) { - Console::info(" Manual config file not found, creating empty list"); + if (! file_exists($sourceConfig['configFile'])) { + Console::info(' Manual config file not found, creating empty list'); + return []; } $domains = include $sourceConfig['configFile']; - Console::info(" Loaded " . count($domains) . " domains from manual config"); + Console::info(' Loaded '.count($domains).' domains from manual config'); return $domains; } @@ -656,7 +648,6 @@ function isValidDomain(string $domain): bool } return true; - } catch (\Exception $e) { return false; } @@ -667,9 +658,9 @@ function isValidDomain(string $domain): bool */ function loadCurrentConfig(string $filename): array { - $filepath = CONFIG_DIR . '/' . $filename; - - if (!file_exists($filepath)) { + $filepath = CONFIG_DIR.'/'.$filename; + + if (! file_exists($filepath)) { return []; } @@ -689,7 +680,7 @@ function isConfigUpToDate(array $currentDomains, array $newDomains): bool */ function saveConfig(string $filename, array $domains, string $description): void { - $configFile = CONFIG_DIR . '/' . $filename; + $configFile = CONFIG_DIR.'/'.$filename; $lastUpdated = date('Y-m-d H:i:s'); // Sort domains for consistent output @@ -716,7 +707,7 @@ function saveConfig(string $filename, array $domains, string $description): void $configContent .= "];\n"; // Ensure directory exists - if (!is_dir(dirname($configFile))) { + if (! is_dir(dirname($configFile))) { mkdir(dirname($configFile), 0755, true); } @@ -739,7 +730,7 @@ function saveConfig(string $filename, array $domains, string $description): void function validateGeneratedPhp(string $filepath, array $expectedDomains): void { // Check if file exists and is readable - if (!file_exists($filepath) || !is_readable($filepath)) { + if (! file_exists($filepath) || ! is_readable($filepath)) { throw new \Exception("Generated file is not readable: {$filepath}"); } @@ -751,11 +742,11 @@ function validateGeneratedPhp(string $filepath, array $expectedDomains): void // Try to include the file to check for syntax errors $oldErrorReporting = error_reporting(E_ALL); $oldDisplayErrors = ini_set('display_errors', 0); - + ob_start(); $included = include $filepath; $output = ob_get_clean(); - + error_reporting($oldErrorReporting); ini_set('display_errors', $oldDisplayErrors); @@ -764,7 +755,7 @@ function validateGeneratedPhp(string $filepath, array $expectedDomains): void } // Verify the included data is an array - if (!is_array($included)) { + if (! is_array($included)) { throw new \Exception("Generated file does not return an array: {$filepath}"); } @@ -779,7 +770,7 @@ function validateGeneratedPhp(string $filepath, array $expectedDomains): void } // Check for any output (should be none for a clean PHP file) - if (!empty($output)) { + if (! empty($output)) { throw new \Exception("Generated file produces unexpected output: {$filepath}"); } } @@ -823,10 +814,10 @@ function showDomainStatistics(array $domains): void $topTlds = array_slice($tldStats, 0, 10, true); Console::info('Domain Statistics:'); - Console::info('├─ Known domains: ' . $knownDomains . ' (' . round(($knownDomains / count($domains)) * 100, 1) . '%)'); - Console::info('├─ ICANN domains: ' . $icannDomains . ' (' . round(($icannDomains / count($domains)) * 100, 1) . '%)'); - Console::info('├─ Private domains: ' . $privateDomains . ' (' . round(($privateDomains / count($domains)) * 100, 1) . '%)'); - Console::info('└─ Unknown domains: ' . $unknownDomains . ' (' . round(($unknownDomains / count($domains)) * 100, 1) . '%)'); + Console::info('├─ Known domains: '.$knownDomains.' ('.round(($knownDomains / count($domains)) * 100, 1).'%)'); + Console::info('├─ ICANN domains: '.$icannDomains.' ('.round(($icannDomains / count($domains)) * 100, 1).'%)'); + Console::info('├─ Private domains: '.$privateDomains.' ('.round(($privateDomains / count($domains)) * 100, 1).'%)'); + Console::info('└─ Unknown domains: '.$unknownDomains.' ('.round(($unknownDomains / count($domains)) * 100, 1).'%)'); Console::info('Top 10 TLDs:'); foreach ($topTlds as $tld => $count) { @@ -842,23 +833,23 @@ function showPreview(array $currentDomains, array $newDomains): void $added = array_diff($newDomains, $currentDomains); $removed = array_diff($currentDomains, $newDomains); - if (!empty($added)) { - Console::info('Domains to be added (' . count($added) . '):'); + if (! empty($added)) { + Console::info('Domains to be added ('.count($added).'):'); foreach (array_slice($added, 0, 10) as $domain) { Console::info(" ├─ + {$domain}"); } if (count($added) > 10) { - Console::info(' └─ ... and ' . (count($added) - 10) . ' more'); + Console::info(' └─ ... and '.(count($added) - 10).' more'); } } - if (!empty($removed)) { - Console::info('Domains to be removed (' . count($removed) . '):'); + if (! empty($removed)) { + Console::info('Domains to be removed ('.count($removed).'):'); foreach (array_slice($removed, 0, 10) as $domain) { Console::info(" ├─ - {$domain}"); } if (count($removed) > 10) { - Console::info(' └─ ... and ' . (count($removed) - 10) . ' more'); + Console::info(' └─ ... and '.(count($removed) - 10).' more'); } } } @@ -873,7 +864,7 @@ function showPreview(array $currentDomains, array $newDomains): void ->param('commit', false, new Boolean(true), 'If set will commit changes to config file. Default is false.', true) ->param('force', false, new Boolean(true), 'Force update even if no changes detected. Default is false.', true) ->param('source', '', new Text(100), 'Specific source to update (optional). Leave empty to update all sources.', true) - ->action(function(bool $commit, bool $force, string $source) { + ->action(function (bool $commit, bool $force, string $source) { updateDisposableDomains($commit, $force, $source); }); @@ -884,7 +875,7 @@ function showPreview(array $currentDomains, array $newDomains): void ->param('commit', false, new Boolean(true), 'If set will commit changes to config file. Default is false.', true) ->param('force', false, new Boolean(true), 'Force update even if no changes detected. Default is false.', true) ->param('source', '', new Text(100), 'Specific source to update (optional). Leave empty to update all sources.', true) - ->action(function(bool $commit, bool $force, string $source) { + ->action(function (bool $commit, bool $force, string $source) { updateFreeDomains($commit, $force, $source); }); @@ -894,7 +885,7 @@ function showPreview(array $currentDomains, array $newDomains): void ->desc('Update both disposable and free email domains from all sources') ->param('commit', false, new Boolean(true), 'If set will commit changes to config file. Default is false.', true) ->param('force', false, new Boolean(true), 'Force update even if no changes detected. Default is false.', true) - ->action(function(bool $commit, bool $force) { + ->action(function (bool $commit, bool $force) { updateAllDomains($commit, $force); }); @@ -902,9 +893,9 @@ function showPreview(array $currentDomains, array $newDomains): void $cli ->task('stats') ->desc('Show statistics about current domain lists') - ->action(function() { + ->action(function () { showStats(); }); // Run the CLI -$cli->run(); \ No newline at end of file +$cli->run(); diff --git a/test-import.php b/test-import.php deleted file mode 100644 index a953633..0000000 --- a/test-import.php +++ /dev/null @@ -1,133 +0,0 @@ -domain = $domain; - } - - public function isTest() { return false; } - public function getName() { - $parts = explode('.', $this->domain); - return $parts[0]; - } - public function getTLD() { - $parts = explode('.', $this->domain); - return end($parts); - } - public function isKnown() { return true; } - public function isICANN() { return true; } - public function isPrivate() { return false; } -} - -// Test domain validation -function isValidDomain($domain) { - if (empty($domain)) { - return false; - } - - try { - $domainObj = new MockDomain($domain); - - if ($domainObj->isTest()) { - return false; - } - - $name = $domainObj->getName(); - $tld = $domainObj->getTLD(); - - if (empty($name) || empty($tld)) { - return false; - } - - return true; - - } catch (Exception $e) { - return false; - } -} - -// Test the import system -echo "Testing Email Domains Import System\n"; -echo "===================================\n\n"; - -// Test domain validation -$testDomains = [ - 'gmail.com' => true, - 'test.com' => true, - 'invalid' => false, - '' => false, - 'subdomain.example.com' => true, - 'test' => false, -]; - -MockCLI::info('Testing domain validation...'); -foreach ($testDomains as $domain => $expected) { - $result = isValidDomain($domain); - $status = $result === $expected ? 'PASS' : 'FAIL'; - echo " $status: '$domain' -> " . ($result ? 'valid' : 'invalid') . "\n"; -} - -// Test source configuration loading -MockCLI::info('Testing source configuration...'); -if (file_exists(__DIR__ . '/data/sources.php')) { - $sources = include __DIR__ . '/data/sources.php'; - echo " ✓ Sources configuration loaded\n"; - echo " - Disposable sources: " . count($sources['disposable']) . "\n"; - echo " - Free sources: " . count($sources['free']) . "\n"; -} else { - echo " ✗ Sources configuration not found\n"; -} - -// Test manual free domains -MockCLI::info('Testing manual free domains...'); -if (file_exists(__DIR__ . '/data/free-domains-manual.php')) { - $domains = include __DIR__ . '/data/free-domains-manual.php'; - echo " ✓ Manual free domains loaded: " . count($domains) . " domains\n"; -} else { - echo " ✗ Manual free domains not found\n"; -} - -// Test manual disposable domains -MockCLI::info('Testing manual disposable domains...'); -if (file_exists(__DIR__ . '/data/disposable-domains-manual.php')) { - $domains = include __DIR__ . '/data/disposable-domains-manual.php'; - echo " ✓ Manual disposable domains loaded: " . count($domains) . " domains\n"; -} else { - echo " ✗ Manual disposable domains not found\n"; -} - -// Test existing domain files -MockCLI::info('Testing existing domain files...'); -if (file_exists(__DIR__ . '/data/disposable-domains.php')) { - $domains = include __DIR__ . '/data/disposable-domains.php'; - echo " ✓ Disposable domains loaded: " . count($domains) . " domains\n"; -} else { - echo " ⚠ Disposable domains not found (will be created on first import)\n"; -} - -if (file_exists(__DIR__ . '/data/free-domains.php')) { - $domains = include __DIR__ . '/data/free-domains.php'; - echo " ✓ Free domains loaded: " . count($domains) . " domains\n"; -} else { - echo " ⚠ Free domains not found (will be created on first import)\n"; -} - -MockCLI::success('Test completed successfully!'); -MockCLI::info('To run the actual import, install dependencies and run: php import.php stats'); diff --git a/tests/EmailTest.php b/tests/EmailTest.php index 4233029..ab20b45 100644 --- a/tests/EmailTest.php +++ b/tests/EmailTest.php @@ -172,7 +172,7 @@ public function testEmailNormalization(): void public function testInvalidEmailEmpty(): void { $this->expectException(Exception::class); - $this->expectExceptionMessage("Email address cannot be empty"); + $this->expectExceptionMessage('Email address cannot be empty'); new Email(''); } @@ -233,15 +233,15 @@ public function testInvalidEmailEndsWithDot(): void public function testInvalidEmailLocalTooLong(): void { $longLocal = str_repeat('a', 65); // 65 characters - $email = new Email($longLocal . '@example.com'); + $email = new Email($longLocal.'@example.com'); $this->assertEquals(false, $email->hasValidLocal()); } public function testInvalidEmailDomainTooLong(): void { - $longDomain = str_repeat('a', 250) . '.com'; // 254 characters - $email = new Email('user@' . $longDomain); + $longDomain = str_repeat('a', 250).'.com'; // 254 characters + $email = new Email('user@'.$longDomain); $this->assertEquals(false, $email->hasValidDomain()); } @@ -328,11 +328,11 @@ public function testFreeEmailProviders(): void 'web.de', 'tutanota.com', 'fastmail.com', - 'hey.com' + 'hey.com', ]; foreach ($freeProviders as $provider) { - $email = new Email('user@' . $provider); + $email = new Email('user@'.$provider); $this->assertEquals(true, $email->isFree(), "Failed for provider: {$provider}"); $this->assertEquals(false, $email->isCorporate(), "Failed for provider: {$provider}"); } @@ -351,11 +351,11 @@ public function testDisposableEmailProviders(): void 'getnada.com', 'maildrop.cc', 'sharklasers.com', - 'test.com' + 'test.com', ]; foreach ($disposableProviders as $provider) { - $email = new Email('user@' . $provider); + $email = new Email('user@'.$provider); $this->assertEquals(true, $email->isDisposable(), "Failed for provider: {$provider}"); $this->assertEquals(false, $email->isCorporate(), "Failed for provider: {$provider}"); } @@ -371,11 +371,11 @@ public function testCorporateEmailProviders(): void 'organization.org', 'firm.com', 'office.net', - 'work.org' + 'work.org', ]; foreach ($corporateProviders as $provider) { - $email = new Email('user@' . $provider); + $email = new Email('user@'.$provider); $this->assertEquals(false, $email->isFree(), "Failed for provider: {$provider}"); $this->assertEquals(false, $email->isDisposable(), "Failed for provider: {$provider}"); $this->assertEquals(true, $email->isCorporate(), "Failed for provider: {$provider}"); diff --git a/tests/Validator/EmailLocalTest.php b/tests/Validator/EmailLocalTest.php index 2da3dca..c0da560 100644 --- a/tests/Validator/EmailLocalTest.php +++ b/tests/Validator/EmailLocalTest.php @@ -76,4 +76,3 @@ public function testValidatorIsArray(): void $this->assertEquals(false, $validator->isArray()); } } - From 3c52d739dcfea03f9408672971dd7b2370a3f7ec Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 18 Oct 2025 22:51:34 +0100 Subject: [PATCH 04/21] Update dependencies and refactor code for consistency and readability --- composer.json | 2 +- data/disposable-domains-manual.php | 4 +- data/disposable-domains.php | 4 +- data/free-domains.php | 4 +- import.php | 14 ++--- src/Emails/Email.php | 24 +++---- src/Emails/Validator/Email.php | 12 +--- src/Emails/Validator/EmailCorporate.php | 13 +--- src/Emails/Validator/EmailDomain.php | 13 +--- src/Emails/Validator/EmailLocal.php | 13 +--- src/Emails/Validator/EmailNotDisposable.php | 15 ++--- tests/EmailTest.php | 69 +++++++++++---------- tests/Validator/EmailCorporateTest.php | 35 ++++++----- tests/Validator/EmailDomainTest.php | 27 ++++---- tests/Validator/EmailLocalTest.php | 27 ++++---- tests/Validator/EmailNotDisposableTest.php | 31 ++++----- tests/Validator/EmailTest.php | 27 ++++---- 17 files changed, 153 insertions(+), 181 deletions(-) diff --git a/composer.json b/composer.json index cff268e..dc9dc37 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ }, "require-dev": { "phpunit/phpunit": "^9.3", - "laravel/pint": "1.2.*", + "laravel/pint": "1.25.*", "phpstan/phpstan": "1.9.x-dev" }, "minimum-stability": "stable", diff --git a/data/disposable-domains-manual.php b/data/disposable-domains-manual.php index 3c1e6ab..5e8450e 100644 --- a/data/disposable-domains-manual.php +++ b/data/disposable-domains-manual.php @@ -2,10 +2,10 @@ /** * Manual Disposable Email Domains Configuration - * + * * This file contains manually managed disposable email domains. * You can add or remove domains here as needed. - * + * * Last updated: 2024-01-01 00:00:00 * Source: Manual configuration */ diff --git a/data/disposable-domains.php b/data/disposable-domains.php index e328d75..d0316ee 100644 --- a/data/disposable-domains.php +++ b/data/disposable-domains.php @@ -2,10 +2,10 @@ /** * Disposable Email Domains - * + * * This file contains a list of known Disposable Email Domains. * Last updated: 2025-10-18 21:39:42 - * + * * Format: Indexed array of domain names */ diff --git a/data/free-domains.php b/data/free-domains.php index c82dc3d..a9a52f3 100644 --- a/data/free-domains.php +++ b/data/free-domains.php @@ -2,10 +2,10 @@ /** * Free Email Domains - * + * * This file contains a list of known Free Email Domains. * Last updated: 2025-10-18 21:39:43 - * + * * Format: Indexed array of domain names */ diff --git a/import.php b/import.php index d558c83..78fe78f 100644 --- a/import.php +++ b/import.php @@ -307,7 +307,7 @@ function fetchSource(string $sourceKey, array $sourceConfig, string $type): arra function fetchMartensonDomains(array $sourceConfig): array { try { - $client = new \Utopia\Fetch\Client(); + $client = new \Utopia\Fetch\Client; $response = $client->fetch( url: $sourceConfig['url'], @@ -353,7 +353,7 @@ function fetchMartensonDomains(array $sourceConfig): array function fetchDisposableDomains(array $sourceConfig): array { try { - $client = new \Utopia\Fetch\Client(); + $client = new \Utopia\Fetch\Client; $response = $client->fetch( url: $sourceConfig['url'], @@ -400,7 +400,7 @@ function fetchDisposableDomains(array $sourceConfig): array function fetchWesbosDomains(array $sourceConfig): array { try { - $client = new \Utopia\Fetch\Client(); + $client = new \Utopia\Fetch\Client; $response = $client->fetch( url: $sourceConfig['url'], @@ -447,7 +447,7 @@ function fetchWesbosDomains(array $sourceConfig): array function fetchFakeFilterDomains(array $sourceConfig): array { try { - $client = new \Utopia\Fetch\Client(); + $client = new \Utopia\Fetch\Client; $response = $client->fetch( url: $sourceConfig['url'], @@ -493,7 +493,7 @@ function fetchFakeFilterDomains(array $sourceConfig): array function fetchAdamLovingDomains(array $sourceConfig): array { try { - $client = new \Utopia\Fetch\Client(); + $client = new \Utopia\Fetch\Client; $response = $client->fetch( url: $sourceConfig['url'], @@ -557,7 +557,7 @@ function loadManualDisposableDomains(array $sourceConfig): array function fetchKikobeatsDomains(array $sourceConfig): array { try { - $client = new \Utopia\Fetch\Client(); + $client = new \Utopia\Fetch\Client; $response = $client->fetch( url: $sourceConfig['url'], @@ -855,7 +855,7 @@ function showPreview(array $currentDomains, array $newDomains): void } // Setup CLI -$cli = new CLI(); +$cli = new CLI; // Disposable domains command $cli diff --git a/src/Emails/Email.php b/src/Emails/Email.php index 0b6f249..eafa5fe 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -64,22 +64,22 @@ class Email public function __construct(string $email) { $this->email = \mb_strtolower(\trim($email)); - + if (empty($this->email)) { - throw new Exception("Email address cannot be empty"); + throw new Exception('Email address cannot be empty'); } $this->parts = \explode('@', $this->email); if (count($this->parts) !== 2) { - throw new Exception("'{$email}' must be a valid email address"); + throw new Exception('{$email} must be a valid email address'); } $this->local = $this->parts[0]; $this->domain = $this->parts[1]; if (empty($this->local) || empty($this->domain)) { - throw new Exception("'{$email}' must be a valid email address"); + throw new Exception('{$email} must be a valid email address'); } } @@ -142,7 +142,7 @@ public function hasValidLocal(): bool } // Check for valid characters in local part - if (!preg_match('/^[a-zA-Z0-9._+-]+$/', $this->local)) { + if (! preg_match('/^[a-zA-Z0-9._+-]+$/', $this->local)) { return false; } @@ -170,7 +170,7 @@ public function hasValidDomain(): bool } // Check for valid domain format using filter_var - if (!filter_var('test@' . $this->domain, FILTER_VALIDATE_EMAIL)) { + if (! filter_var('test@'.$this->domain, FILTER_VALIDATE_EMAIL)) { return false; } @@ -183,7 +183,7 @@ public function hasValidDomain(): bool public function isDisposable(): bool { if (self::$disposableDomains === null) { - self::$disposableDomains = include __DIR__ . '/../../data/disposable-domains.php'; + self::$disposableDomains = include __DIR__.'/../../data/disposable-domains.php'; } return in_array($this->domain, self::$disposableDomains); @@ -195,7 +195,7 @@ public function isDisposable(): bool public function isFree(): bool { if (self::$freeDomains === null) { - self::$freeDomains = include __DIR__ . '/../../data/free-domains.php'; + self::$freeDomains = include __DIR__.'/../../data/free-domains.php'; } return in_array($this->domain, self::$freeDomains); @@ -206,7 +206,7 @@ public function isFree(): bool */ public function isCorporate(): bool { - return !$this->isFree() && !$this->isDisposable(); + return ! $this->isFree() && ! $this->isDisposable(); } /** @@ -215,7 +215,7 @@ public function isCorporate(): bool public function getProvider(): string { $domainParts = explode('.', $this->domain); - + if (count($domainParts) < 2) { return $this->domain; } @@ -234,7 +234,7 @@ public function getProvider(): string public function getSubdomain(): string { $domainParts = explode('.', $this->domain); - + if (count($domainParts) <= 2) { return ''; } @@ -247,7 +247,7 @@ public function getSubdomain(): string */ public function hasSubdomain(): bool { - return !empty($this->getSubdomain()); + return ! empty($this->getSubdomain()); } /** diff --git a/src/Emails/Validator/Email.php b/src/Emails/Validator/Email.php index 7793d14..4bffadc 100644 --- a/src/Emails/Validator/Email.php +++ b/src/Emails/Validator/Email.php @@ -16,8 +16,6 @@ class Email extends Validator * Get Description * * Returns validator description - * - * @return string */ public function getDescription(): string { @@ -29,17 +27,17 @@ public function getDescription(): string * * Validation will pass when $value is a valid email address * - * @param mixed $value - * @return bool + * @param mixed $value */ public function isValid($value): bool { - if (!is_string($value)) { + if (! is_string($value)) { return false; } try { $email = new EmailParser($value); + return $email->isValid(); } catch (\Exception $e) { return false; @@ -50,8 +48,6 @@ public function isValid($value): bool * Is array * * Function will return true if object is array. - * - * @return bool */ public function isArray(): bool { @@ -62,8 +58,6 @@ public function isArray(): bool * Get Type * * Returns validator type. - * - * @return string */ public function getType(): string { diff --git a/src/Emails/Validator/EmailCorporate.php b/src/Emails/Validator/EmailCorporate.php index 67e43c3..44a9683 100644 --- a/src/Emails/Validator/EmailCorporate.php +++ b/src/Emails/Validator/EmailCorporate.php @@ -16,8 +16,6 @@ class EmailCorporate extends Validator * Get Description * * Returns validator description - * - * @return string */ public function getDescription(): string { @@ -29,17 +27,17 @@ public function getDescription(): string * * Validation will pass when $value is a valid email address from a corporate domain * - * @param mixed $value - * @return bool + * @param mixed $value */ public function isValid($value): bool { - if (!is_string($value)) { + if (! is_string($value)) { return false; } try { $email = new Email($value); + return $email->isValid() && $email->isCorporate(); } catch (\Exception $e) { return false; @@ -50,8 +48,6 @@ public function isValid($value): bool * Is array * * Function will return true if object is array. - * - * @return bool */ public function isArray(): bool { @@ -62,12 +58,9 @@ public function isArray(): bool * Get Type * * Returns validator type. - * - * @return string */ public function getType(): string { return self::TYPE_STRING; } } - diff --git a/src/Emails/Validator/EmailDomain.php b/src/Emails/Validator/EmailDomain.php index 0be9164..a63be5e 100644 --- a/src/Emails/Validator/EmailDomain.php +++ b/src/Emails/Validator/EmailDomain.php @@ -16,8 +16,6 @@ class EmailDomain extends Validator * Get Description * * Returns validator description - * - * @return string */ public function getDescription(): string { @@ -29,17 +27,17 @@ public function getDescription(): string * * Validation will pass when $value is a valid email address with a valid domain * - * @param mixed $value - * @return bool + * @param mixed $value */ public function isValid($value): bool { - if (!is_string($value)) { + if (! is_string($value)) { return false; } try { $email = new Email($value); + return $email->isValid() && $email->hasValidDomain(); } catch (\Exception $e) { return false; @@ -50,8 +48,6 @@ public function isValid($value): bool * Is array * * Function will return true if object is array. - * - * @return bool */ public function isArray(): bool { @@ -62,12 +58,9 @@ public function isArray(): bool * Get Type * * Returns validator type. - * - * @return string */ public function getType(): string { return self::TYPE_STRING; } } - diff --git a/src/Emails/Validator/EmailLocal.php b/src/Emails/Validator/EmailLocal.php index 7d586cd..c34d490 100644 --- a/src/Emails/Validator/EmailLocal.php +++ b/src/Emails/Validator/EmailLocal.php @@ -16,8 +16,6 @@ class EmailLocal extends Validator * Get Description * * Returns validator description - * - * @return string */ public function getDescription(): string { @@ -29,17 +27,17 @@ public function getDescription(): string * * Validation will pass when $value is a valid email address with a valid local part * - * @param mixed $value - * @return bool + * @param mixed $value */ public function isValid($value): bool { - if (!is_string($value)) { + if (! is_string($value)) { return false; } try { $email = new Email($value); + return $email->isValid() && $email->hasValidLocal(); } catch (\Exception $e) { return false; @@ -50,8 +48,6 @@ public function isValid($value): bool * Is array * * Function will return true if object is array. - * - * @return bool */ public function isArray(): bool { @@ -62,12 +58,9 @@ public function isArray(): bool * Get Type * * Returns validator type. - * - * @return string */ public function getType(): string { return self::TYPE_STRING; } } - diff --git a/src/Emails/Validator/EmailNotDisposable.php b/src/Emails/Validator/EmailNotDisposable.php index fa4ed3b..a243589 100644 --- a/src/Emails/Validator/EmailNotDisposable.php +++ b/src/Emails/Validator/EmailNotDisposable.php @@ -16,8 +16,6 @@ class EmailNotDisposable extends Validator * Get Description * * Returns validator description - * - * @return string */ public function getDescription(): string { @@ -29,18 +27,18 @@ public function getDescription(): string * * Validation will pass when $value is a valid email address that is not disposable * - * @param mixed $value - * @return bool + * @param mixed $value */ public function isValid($value): bool { - if (!is_string($value)) { + if (! is_string($value)) { return false; } try { $email = new Email($value); - return $email->isValid() && !$email->isDisposable(); + + return $email->isValid() && ! $email->isDisposable(); } catch (\Exception $e) { return false; } @@ -50,8 +48,6 @@ public function isValid($value): bool * Is array * * Function will return true if object is array. - * - * @return bool */ public function isArray(): bool { @@ -62,12 +58,9 @@ public function isArray(): bool * Get Type * * Returns validator type. - * - * @return string */ public function getType(): string { return self::TYPE_STRING; } } - diff --git a/tests/EmailTest.php b/tests/EmailTest.php index ab20b45..7932e4e 100644 --- a/tests/EmailTest.php +++ b/tests/EmailTest.php @@ -1,4 +1,5 @@ assertEquals('test@example.com', $email->normalize()); } - public function testEmailWithSubdomain(): void + public function test_email_with_subdomain(): void { $email = new Email('user@mail.example.com'); @@ -53,7 +54,7 @@ public function testEmailWithSubdomain(): void $this->assertEquals(true, $email->hasSubdomain()); } - public function testGmailEmail(): void + public function test_gmail_email(): void { $email = new Email('user@gmail.com'); @@ -66,7 +67,7 @@ public function testGmailEmail(): void $this->assertEquals('gmail.com', $email->getProvider()); } - public function testDisposableEmail(): void + public function test_disposable_email(): void { $email = new Email('user@10minutemail.com'); @@ -78,7 +79,7 @@ public function testDisposableEmail(): void $this->assertEquals(false, $email->isCorporate()); } - public function testEmailWithSpecialCharacters(): void + public function test_email_with_special_characters(): void { $email = new Email('user.name+tag@example.com'); @@ -90,7 +91,7 @@ public function testEmailWithSpecialCharacters(): void $this->assertEquals(true, $email->hasValidDomain()); } - public function testEmailWithHyphens(): void + public function test_email_with_hyphens(): void { $email = new Email('user-name@example-domain.com'); @@ -102,7 +103,7 @@ public function testEmailWithHyphens(): void $this->assertEquals(true, $email->hasValidDomain()); } - public function testEmailWithUnderscores(): void + public function test_email_with_underscores(): void { $email = new Email('user_name@example.com'); @@ -114,7 +115,7 @@ public function testEmailWithUnderscores(): void $this->assertEquals(true, $email->hasValidDomain()); } - public function testEmailWithNumbers(): void + public function test_email_with_numbers(): void { $email = new Email('user123@example123.com'); @@ -126,7 +127,7 @@ public function testEmailWithNumbers(): void $this->assertEquals(true, $email->hasValidDomain()); } - public function testEmailWithMultipleDots(): void + public function test_email_with_multiple_dots(): void { $email = new Email('user.name.last@example.com'); @@ -138,7 +139,7 @@ public function testEmailWithMultipleDots(): void $this->assertEquals(true, $email->hasValidDomain()); } - public function testEmailWithMultipleSubdomains(): void + public function test_email_with_multiple_subdomains(): void { $email = new Email('user@mail.sub.example.com'); @@ -150,7 +151,7 @@ public function testEmailWithMultipleSubdomains(): void $this->assertEquals(true, $email->hasSubdomain()); } - public function testEmailFormatted(): void + public function test_email_formatted(): void { $email = new Email('user@mail.example.com'); @@ -161,7 +162,7 @@ public function testEmailFormatted(): void $this->assertEquals('mail', $email->getFormatted('subdomain')); } - public function testEmailNormalization(): void + public function test_email_normalization(): void { $email = new Email(' USER@EXAMPLE.COM '); @@ -169,7 +170,7 @@ public function testEmailNormalization(): void $this->assertEquals('user@example.com', $email->normalize()); } - public function testInvalidEmailEmpty(): void + public function test_invalid_email_empty(): void { $this->expectException(Exception::class); $this->expectExceptionMessage('Email address cannot be empty'); @@ -177,7 +178,7 @@ public function testInvalidEmailEmpty(): void new Email(''); } - public function testInvalidEmailNoAt(): void + public function test_invalid_email_no_at(): void { $this->expectException(Exception::class); $this->expectExceptionMessage("'invalid-email' must be a valid email address"); @@ -185,7 +186,7 @@ public function testInvalidEmailNoAt(): void new Email('invalid-email'); } - public function testInvalidEmailMultipleAt(): void + public function test_invalid_email_multiple_at(): void { $this->expectException(Exception::class); $this->expectExceptionMessage("'user@example@com' must be a valid email address"); @@ -193,7 +194,7 @@ public function testInvalidEmailMultipleAt(): void new Email('user@example@com'); } - public function testInvalidEmailNoLocal(): void + public function test_invalid_email_no_local(): void { $this->expectException(Exception::class); $this->expectExceptionMessage("'@example.com' must be a valid email address"); @@ -201,7 +202,7 @@ public function testInvalidEmailNoLocal(): void new Email('@example.com'); } - public function testInvalidEmailNoDomain(): void + public function test_invalid_email_no_domain(): void { $this->expectException(Exception::class); $this->expectExceptionMessage("'user@' must be a valid email address"); @@ -209,28 +210,28 @@ public function testInvalidEmailNoDomain(): void new Email('user@'); } - public function testInvalidEmailConsecutiveDots(): void + public function test_invalid_email_consecutive_dots(): void { $email = new Email('user..name@example.com'); $this->assertEquals(false, $email->hasValidLocal()); } - public function testInvalidEmailStartsWithDot(): void + public function test_invalid_email_starts_with_dot(): void { $email = new Email('.user@example.com'); $this->assertEquals(false, $email->hasValidLocal()); } - public function testInvalidEmailEndsWithDot(): void + public function test_invalid_email_ends_with_dot(): void { $email = new Email('user.@example.com'); $this->assertEquals(false, $email->hasValidLocal()); } - public function testInvalidEmailLocalTooLong(): void + public function test_invalid_email_local_too_long(): void { $longLocal = str_repeat('a', 65); // 65 characters $email = new Email($longLocal.'@example.com'); @@ -238,7 +239,7 @@ public function testInvalidEmailLocalTooLong(): void $this->assertEquals(false, $email->hasValidLocal()); } - public function testInvalidEmailDomainTooLong(): void + public function test_invalid_email_domain_too_long(): void { $longDomain = str_repeat('a', 250).'.com'; // 254 characters $email = new Email('user@'.$longDomain); @@ -246,14 +247,14 @@ public function testInvalidEmailDomainTooLong(): void $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailDomainConsecutiveDots(): void + public function test_invalid_email_domain_consecutive_dots(): void { $email = new Email('user@example..com'); $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailDomainConsecutiveHyphens(): void + public function test_invalid_email_domain_consecutive_hyphens(): void { $email = new Email('user@example--com.com'); @@ -261,56 +262,56 @@ public function testInvalidEmailDomainConsecutiveHyphens(): void $this->assertEquals(true, $email->hasValidDomain()); } - public function testInvalidEmailDomainStartsWithDot(): void + public function test_invalid_email_domain_starts_with_dot(): void { $email = new Email('user@.example.com'); $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailDomainEndsWithDot(): void + public function test_invalid_email_domain_ends_with_dot(): void { $email = new Email('user@example.com.'); $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailDomainStartsWithHyphen(): void + public function test_invalid_email_domain_starts_with_hyphen(): void { $email = new Email('user@-example.com'); $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailDomainEndsWithHyphen(): void + public function test_invalid_email_domain_ends_with_hyphen(): void { $email = new Email('user@example-.com'); $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailDomainNoTLD(): void + public function test_invalid_email_domain_no_tld(): void { $email = new Email('user@example'); $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailDomainInvalidCharacters(): void + public function test_invalid_email_domain_invalid_characters(): void { $email = new Email('user@example!.com'); $this->assertEquals(false, $email->hasValidDomain()); } - public function testInvalidEmailLocalInvalidCharacters(): void + public function test_invalid_email_local_invalid_characters(): void { $email = new Email('user!@example.com'); $this->assertEquals(false, $email->hasValidLocal()); } - public function testFreeEmailProviders(): void + public function test_free_email_providers(): void { $freeProviders = [ 'gmail.com', @@ -338,7 +339,7 @@ public function testFreeEmailProviders(): void } } - public function testDisposableEmailProviders(): void + public function test_disposable_email_providers(): void { $disposableProviders = [ '10minutemail.com', @@ -361,7 +362,7 @@ public function testDisposableEmailProviders(): void } } - public function testCorporateEmailProviders(): void + public function test_corporate_email_providers(): void { $corporateProviders = [ 'company.com', diff --git a/tests/Validator/EmailCorporateTest.php b/tests/Validator/EmailCorporateTest.php index b215912..72aeb82 100644 --- a/tests/Validator/EmailCorporateTest.php +++ b/tests/Validator/EmailCorporateTest.php @@ -1,4 +1,5 @@ assertEquals(true, $validator->isValid('test@company.com')); $this->assertEquals(true, $validator->isValid('user@business.org')); @@ -33,9 +34,9 @@ public function testValidCorporateEmail(): void $this->assertEquals(true, $validator->isValid('user@work.org')); } - public function testInvalidFreeEmail(): void + public function test_invalid_free_email(): void { - $validator = new EmailCorporate(); + $validator = new EmailCorporate; $this->assertEquals(false, $validator->isValid('user@gmail.com')); $this->assertEquals(false, $validator->isValid('user@yahoo.com')); @@ -55,9 +56,9 @@ public function testInvalidFreeEmail(): void $this->assertEquals(false, $validator->isValid('user@hey.com')); } - public function testInvalidDisposableEmail(): void + public function test_invalid_disposable_email(): void { - $validator = new EmailCorporate(); + $validator = new EmailCorporate; $this->assertEquals(false, $validator->isValid('user@10minutemail.com')); $this->assertEquals(false, $validator->isValid('user@tempmail.org')); @@ -76,9 +77,9 @@ public function testInvalidDisposableEmail(): void $this->assertEquals(true, $validator->isValid('user@example.net')); } - public function testInvalidEmailFormat(): void + public function test_invalid_email_format(): void { - $validator = new EmailCorporate(); + $validator = new EmailCorporate; $this->assertEquals(false, $validator->isValid('')); $this->assertEquals(false, $validator->isValid('invalid-email')); @@ -87,35 +88,35 @@ public function testInvalidEmailFormat(): void $this->assertEquals(false, $validator->isValid('user@')); } - public function testNonStringInput(): void + public function test_non_string_input(): void { - $validator = new EmailCorporate(); + $validator = new EmailCorporate; $this->assertEquals(false, $validator->isValid(null)); $this->assertEquals(false, $validator->isValid(123)); $this->assertEquals(false, $validator->isValid([])); - $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(new \stdClass)); $this->assertEquals(false, $validator->isValid(true)); $this->assertEquals(false, $validator->isValid(false)); } - public function testValidatorDescription(): void + public function test_validator_description(): void { - $validator = new EmailCorporate(); + $validator = new EmailCorporate; $this->assertEquals('Value must be a valid email address from a corporate domain', $validator->getDescription()); } - public function testValidatorType(): void + public function test_validator_type(): void { - $validator = new EmailCorporate(); + $validator = new EmailCorporate; $this->assertEquals('string', $validator->getType()); } - public function testValidatorIsArray(): void + public function test_validator_is_array(): void { - $validator = new EmailCorporate(); + $validator = new EmailCorporate; $this->assertEquals(false, $validator->isArray()); } diff --git a/tests/Validator/EmailDomainTest.php b/tests/Validator/EmailDomainTest.php index 6e5f7ee..ff7fee1 100644 --- a/tests/Validator/EmailDomainTest.php +++ b/tests/Validator/EmailDomainTest.php @@ -1,4 +1,5 @@ assertEquals(true, $validator->isValid('test@example.com')); $this->assertEquals(true, $validator->isValid('user@mail.example.com')); @@ -30,9 +31,9 @@ public function testValidEmailDomain(): void $this->assertEquals(true, $validator->isValid('user@example123.com')); } - public function testInvalidEmailDomain(): void + public function test_invalid_email_domain(): void { - $validator = new EmailDomain(); + $validator = new EmailDomain; $this->assertEquals(false, $validator->isValid('')); $this->assertEquals(false, $validator->isValid('invalid-email')); @@ -47,35 +48,35 @@ public function testInvalidEmailDomain(): void $this->assertEquals(false, $validator->isValid('user@example!.com')); } - public function testNonStringInput(): void + public function test_non_string_input(): void { - $validator = new EmailDomain(); + $validator = new EmailDomain; $this->assertEquals(false, $validator->isValid(null)); $this->assertEquals(false, $validator->isValid(123)); $this->assertEquals(false, $validator->isValid([])); - $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(new \stdClass)); $this->assertEquals(false, $validator->isValid(true)); $this->assertEquals(false, $validator->isValid(false)); } - public function testValidatorDescription(): void + public function test_validator_description(): void { - $validator = new EmailDomain(); + $validator = new EmailDomain; $this->assertEquals('Value must be a valid email address with a valid domain', $validator->getDescription()); } - public function testValidatorType(): void + public function test_validator_type(): void { - $validator = new EmailDomain(); + $validator = new EmailDomain; $this->assertEquals('string', $validator->getType()); } - public function testValidatorIsArray(): void + public function test_validator_is_array(): void { - $validator = new EmailDomain(); + $validator = new EmailDomain; $this->assertEquals(false, $validator->isArray()); } diff --git a/tests/Validator/EmailLocalTest.php b/tests/Validator/EmailLocalTest.php index c0da560..3b22224 100644 --- a/tests/Validator/EmailLocalTest.php +++ b/tests/Validator/EmailLocalTest.php @@ -1,4 +1,5 @@ assertEquals(true, $validator->isValid('test@example.com')); $this->assertEquals(true, $validator->isValid('user.name+tag@example.com')); @@ -31,9 +32,9 @@ public function testValidEmailLocal(): void $this->assertEquals(true, $validator->isValid('user.name.last@example.com')); } - public function testInvalidEmailLocal(): void + public function test_invalid_email_local(): void { - $validator = new EmailLocal(); + $validator = new EmailLocal; $this->assertEquals(false, $validator->isValid('')); $this->assertEquals(false, $validator->isValid('invalid-email')); @@ -43,35 +44,35 @@ public function testInvalidEmailLocal(): void $this->assertEquals(false, $validator->isValid('user!@example.com')); } - public function testNonStringInput(): void + public function test_non_string_input(): void { - $validator = new EmailLocal(); + $validator = new EmailLocal; $this->assertEquals(false, $validator->isValid(null)); $this->assertEquals(false, $validator->isValid(123)); $this->assertEquals(false, $validator->isValid([])); - $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(new \stdClass)); $this->assertEquals(false, $validator->isValid(true)); $this->assertEquals(false, $validator->isValid(false)); } - public function testValidatorDescription(): void + public function test_validator_description(): void { - $validator = new EmailLocal(); + $validator = new EmailLocal; $this->assertEquals('Value must be a valid email address with a valid local part', $validator->getDescription()); } - public function testValidatorType(): void + public function test_validator_type(): void { - $validator = new EmailLocal(); + $validator = new EmailLocal; $this->assertEquals('string', $validator->getType()); } - public function testValidatorIsArray(): void + public function test_validator_is_array(): void { - $validator = new EmailLocal(); + $validator = new EmailLocal; $this->assertEquals(false, $validator->isArray()); } diff --git a/tests/Validator/EmailNotDisposableTest.php b/tests/Validator/EmailNotDisposableTest.php index 6ff402d..ef8ee6f 100644 --- a/tests/Validator/EmailNotDisposableTest.php +++ b/tests/Validator/EmailNotDisposableTest.php @@ -1,4 +1,5 @@ assertEquals(true, $validator->isValid('test@example.com')); $this->assertEquals(true, $validator->isValid('user@gmail.com')); @@ -30,9 +31,9 @@ public function testValidNonDisposableEmail(): void $this->assertEquals(true, $validator->isValid('user@business.org')); } - public function testInvalidDisposableEmail(): void + public function test_invalid_disposable_email(): void { - $validator = new EmailNotDisposable(); + $validator = new EmailNotDisposable; $this->assertEquals(false, $validator->isValid('user@10minutemail.com')); $this->assertEquals(false, $validator->isValid('user@tempmail.org')); @@ -51,9 +52,9 @@ public function testInvalidDisposableEmail(): void $this->assertEquals(true, $validator->isValid('user@example.net')); } - public function testInvalidEmailFormat(): void + public function test_invalid_email_format(): void { - $validator = new EmailNotDisposable(); + $validator = new EmailNotDisposable; $this->assertEquals(false, $validator->isValid('')); $this->assertEquals(false, $validator->isValid('invalid-email')); @@ -62,35 +63,35 @@ public function testInvalidEmailFormat(): void $this->assertEquals(false, $validator->isValid('user@')); } - public function testNonStringInput(): void + public function test_non_string_input(): void { - $validator = new EmailNotDisposable(); + $validator = new EmailNotDisposable; $this->assertEquals(false, $validator->isValid(null)); $this->assertEquals(false, $validator->isValid(123)); $this->assertEquals(false, $validator->isValid([])); - $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(new \stdClass)); $this->assertEquals(false, $validator->isValid(true)); $this->assertEquals(false, $validator->isValid(false)); } - public function testValidatorDescription(): void + public function test_validator_description(): void { - $validator = new EmailNotDisposable(); + $validator = new EmailNotDisposable; $this->assertEquals('Value must be a valid email address that is not from a disposable email service', $validator->getDescription()); } - public function testValidatorType(): void + public function test_validator_type(): void { - $validator = new EmailNotDisposable(); + $validator = new EmailNotDisposable; $this->assertEquals('string', $validator->getType()); } - public function testValidatorIsArray(): void + public function test_validator_is_array(): void { - $validator = new EmailNotDisposable(); + $validator = new EmailNotDisposable; $this->assertEquals(false, $validator->isArray()); } diff --git a/tests/Validator/EmailTest.php b/tests/Validator/EmailTest.php index f9b2de4..ff4121e 100644 --- a/tests/Validator/EmailTest.php +++ b/tests/Validator/EmailTest.php @@ -1,4 +1,5 @@ assertEquals(true, $validator->isValid('test@example.com')); $this->assertEquals(true, $validator->isValid('user.name+tag@example.com')); @@ -33,9 +34,9 @@ public function testValidEmail(): void $this->assertEquals(true, $validator->isValid('user@mail.sub.example.com')); } - public function testInvalidEmail(): void + public function test_invalid_email(): void { - $validator = new Email(); + $validator = new Email; $this->assertEquals(false, $validator->isValid('')); $this->assertEquals(false, $validator->isValid('invalid-email')); @@ -58,35 +59,35 @@ public function testInvalidEmail(): void $this->assertEquals(true, $validator->isValid('user!@example.com')); } - public function testNonStringInput(): void + public function test_non_string_input(): void { - $validator = new Email(); + $validator = new Email; $this->assertEquals(false, $validator->isValid(null)); $this->assertEquals(false, $validator->isValid(123)); $this->assertEquals(false, $validator->isValid([])); - $this->assertEquals(false, $validator->isValid(new \stdClass())); + $this->assertEquals(false, $validator->isValid(new \stdClass)); $this->assertEquals(false, $validator->isValid(true)); $this->assertEquals(false, $validator->isValid(false)); } - public function testValidatorDescription(): void + public function test_validator_description(): void { - $validator = new Email(); + $validator = new Email; $this->assertEquals('Value must be a valid email address', $validator->getDescription()); } - public function testValidatorType(): void + public function test_validator_type(): void { - $validator = new Email(); + $validator = new Email; $this->assertEquals('string', $validator->getType()); } - public function testValidatorIsArray(): void + public function test_validator_is_array(): void { - $validator = new Email(); + $validator = new Email; $this->assertEquals(false, $validator->isArray()); } From 88f35d7da92a88ec95e61d1f5f111b2c6b0ca116 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 18 Oct 2025 22:54:24 +0100 Subject: [PATCH 05/21] Update last updated timestamps in domain files and adjust comment formatting in import.php --- data/disposable-domains.php | 2 +- data/free-domains.php | 2 +- import.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/disposable-domains.php b/data/disposable-domains.php index d0316ee..22cab0f 100644 --- a/data/disposable-domains.php +++ b/data/disposable-domains.php @@ -4,7 +4,7 @@ * Disposable Email Domains * * This file contains a list of known Disposable Email Domains. - * Last updated: 2025-10-18 21:39:42 + * Last updated: 2025-10-18 21:53:41 * * Format: Indexed array of domain names */ diff --git a/data/free-domains.php b/data/free-domains.php index a9a52f3..8573b8d 100644 --- a/data/free-domains.php +++ b/data/free-domains.php @@ -4,7 +4,7 @@ * Free Email Domains * * This file contains a list of known Free Email Domains. - * Last updated: 2025-10-18 21:39:43 + * Last updated: 2025-10-18 21:53:53 * * Format: Indexed array of domain names */ diff --git a/import.php b/import.php index 78fe78f..1f5c896 100644 --- a/import.php +++ b/import.php @@ -689,10 +689,10 @@ function saveConfig(string $filename, array $domains, string $description): void $configContent = " Date: Sat, 18 Oct 2025 23:06:56 +0100 Subject: [PATCH 06/21] Add new disposable and free email domains, update last updated timestamps, and improve exception message formatting --- data/disposable-domains-manual.php | 3 ++ data/disposable-domains.php | 4 +- data/free-domains-manual.php | 2 + data/free-domains.php | 4 +- src/Emails/Email.php | 14 ++++- tests/EmailTest.php | 60 +++++++++++----------- tests/Validator/EmailCorporateTest.php | 8 +-- tests/Validator/EmailNotDisposableTest.php | 10 ++-- 8 files changed, 62 insertions(+), 43 deletions(-) diff --git a/data/disposable-domains-manual.php b/data/disposable-domains-manual.php index 5e8450e..c148c08 100644 --- a/data/disposable-domains-manual.php +++ b/data/disposable-domains-manual.php @@ -11,4 +11,7 @@ */ return [ + 'tempmail.org', + 'throwaway.email', + '10minutemail.com', ]; diff --git a/data/disposable-domains.php b/data/disposable-domains.php index 22cab0f..14c1fc2 100644 --- a/data/disposable-domains.php +++ b/data/disposable-domains.php @@ -4,7 +4,7 @@ * Disposable Email Domains * * This file contains a list of known Disposable Email Domains. - * Last updated: 2025-10-18 21:53:41 + * Last updated: 2025-10-18 22:03:31 * * Format: Indexed array of domain names */ @@ -61642,6 +61642,7 @@ 'tempmail.j78.org', 'tempmail.net', 'tempmail.ninja', + 'tempmail.org', 'tempmail.plus', 'tempmail.pp.ua', 'tempmail.pro', @@ -62452,6 +62453,7 @@ 'throopllc.com', 'thrott.com', 'throwam.com', + 'throwaway.email', 'throwaway.io', 'throwawayemail.com', 'throwawayemailaddress.com', diff --git a/data/free-domains-manual.php b/data/free-domains-manual.php index 0aa81b4..fb4397e 100644 --- a/data/free-domains-manual.php +++ b/data/free-domains-manual.php @@ -13,4 +13,6 @@ return [ 'walla.co.il', 'walla.com', + 'tutanota.com', + 'hey.com', ]; diff --git a/data/free-domains.php b/data/free-domains.php index 8573b8d..6943c57 100644 --- a/data/free-domains.php +++ b/data/free-domains.php @@ -4,7 +4,7 @@ * Free Email Domains * * This file contains a list of known Free Email Domains. - * Last updated: 2025-10-18 21:53:53 + * Last updated: 2025-10-18 22:02:16 * * Format: Indexed array of domain names */ @@ -1339,6 +1339,7 @@ 'herp.in', 'herr-der-mails.de', 'hetnet.nl', + 'hey.com', 'hey.to', 'hhdevel.com', 'hidzz.com', @@ -4374,6 +4375,7 @@ 'turkey.com', 'turual.com', 'tut.by', + 'tutanota.com', 'tvstar.com', 'twc.com', 'twcny.com', diff --git a/src/Emails/Email.php b/src/Emails/Email.php index eafa5fe..aaad7da 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -72,14 +72,14 @@ public function __construct(string $email) $this->parts = \explode('@', $this->email); if (count($this->parts) !== 2) { - throw new Exception('{$email} must be a valid email address'); + throw new Exception("'{$email}' must be a valid email address"); } $this->local = $this->parts[0]; $this->domain = $this->parts[1]; if (empty($this->local) || empty($this->domain)) { - throw new Exception('{$email} must be a valid email address'); + throw new Exception("'{$email}' must be a valid email address"); } } @@ -198,6 +198,11 @@ public function isFree(): bool self::$freeDomains = include __DIR__.'/../../data/free-domains.php'; } + // If domain is both free and disposable, prioritize disposable classification + if (in_array($this->domain, self::$freeDomains) && $this->isDisposable()) { + return false; // It's disposable, not free + } + return in_array($this->domain, self::$freeDomains); } @@ -206,6 +211,11 @@ public function isFree(): bool */ public function isCorporate(): bool { + // If domain is both free and disposable, prioritize free classification + if ($this->isFree() && $this->isDisposable()) { + return false; // It's free, not corporate + } + return ! $this->isFree() && ! $this->isDisposable(); } diff --git a/tests/EmailTest.php b/tests/EmailTest.php index 7932e4e..c8f4bbd 100644 --- a/tests/EmailTest.php +++ b/tests/EmailTest.php @@ -23,12 +23,12 @@ class EmailTest extends TestCase { public function test_valid_email(): void { - $email = new Email('test@example.com'); + $email = new Email('test@company.org'); - $this->assertEquals('test@example.com', $email->get()); + $this->assertEquals('test@company.org', $email->get()); $this->assertEquals('test', $email->getLocal()); - $this->assertEquals('example.com', $email->getDomain()); - $this->assertEquals('example.com', $email->getDomainOnly()); + $this->assertEquals('company.org', $email->getDomain()); + $this->assertEquals('company.org', $email->getDomainOnly()); $this->assertEquals('test', $email->getLocalOnly()); $this->assertEquals(true, $email->isValid()); $this->assertEquals(true, $email->hasValidLocal()); @@ -36,20 +36,20 @@ public function test_valid_email(): void $this->assertEquals(false, $email->isDisposable()); $this->assertEquals(false, $email->isFree()); $this->assertEquals(true, $email->isCorporate()); - $this->assertEquals('example.com', $email->getProvider()); + $this->assertEquals('company.org', $email->getProvider()); $this->assertEquals('', $email->getSubdomain()); $this->assertEquals(false, $email->hasSubdomain()); - $this->assertEquals('test@example.com', $email->normalize()); + $this->assertEquals('test@company.org', $email->normalize()); } public function test_email_with_subdomain(): void { - $email = new Email('user@mail.example.com'); + $email = new Email('user@mail.company.org'); - $this->assertEquals('user@mail.example.com', $email->get()); + $this->assertEquals('user@mail.company.org', $email->get()); $this->assertEquals('user', $email->getLocal()); - $this->assertEquals('mail.example.com', $email->getDomain()); - $this->assertEquals('example.com', $email->getProvider()); + $this->assertEquals('mail.company.org', $email->getDomain()); + $this->assertEquals('company.org', $email->getProvider()); $this->assertEquals('mail', $email->getSubdomain()); $this->assertEquals(true, $email->hasSubdomain()); } @@ -81,11 +81,11 @@ public function test_disposable_email(): void public function test_email_with_special_characters(): void { - $email = new Email('user.name+tag@example.com'); + $email = new Email('user.name+tag@company.org'); - $this->assertEquals('user.name+tag@example.com', $email->get()); + $this->assertEquals('user.name+tag@company.org', $email->get()); $this->assertEquals('user.name+tag', $email->getLocal()); - $this->assertEquals('example.com', $email->getDomain()); + $this->assertEquals('company.org', $email->getDomain()); $this->assertEquals(true, $email->isValid()); $this->assertEquals(true, $email->hasValidLocal()); $this->assertEquals(true, $email->hasValidDomain()); @@ -105,11 +105,11 @@ public function test_email_with_hyphens(): void public function test_email_with_underscores(): void { - $email = new Email('user_name@example.com'); + $email = new Email('user_name@company.org'); - $this->assertEquals('user_name@example.com', $email->get()); + $this->assertEquals('user_name@company.org', $email->get()); $this->assertEquals('user_name', $email->getLocal()); - $this->assertEquals('example.com', $email->getDomain()); + $this->assertEquals('company.org', $email->getDomain()); $this->assertEquals(true, $email->isValid()); $this->assertEquals(true, $email->hasValidLocal()); $this->assertEquals(true, $email->hasValidDomain()); @@ -129,11 +129,11 @@ public function test_email_with_numbers(): void public function test_email_with_multiple_dots(): void { - $email = new Email('user.name.last@example.com'); + $email = new Email('user.name.last@company.org'); - $this->assertEquals('user.name.last@example.com', $email->get()); + $this->assertEquals('user.name.last@company.org', $email->get()); $this->assertEquals('user.name.last', $email->getLocal()); - $this->assertEquals('example.com', $email->getDomain()); + $this->assertEquals('company.org', $email->getDomain()); $this->assertEquals(true, $email->isValid()); $this->assertEquals(true, $email->hasValidLocal()); $this->assertEquals(true, $email->hasValidDomain()); @@ -141,33 +141,33 @@ public function test_email_with_multiple_dots(): void public function test_email_with_multiple_subdomains(): void { - $email = new Email('user@mail.sub.example.com'); + $email = new Email('user@mail.sub.company.org'); - $this->assertEquals('user@mail.sub.example.com', $email->get()); + $this->assertEquals('user@mail.sub.company.org', $email->get()); $this->assertEquals('user', $email->getLocal()); - $this->assertEquals('mail.sub.example.com', $email->getDomain()); - $this->assertEquals('example.com', $email->getProvider()); + $this->assertEquals('mail.sub.company.org', $email->getDomain()); + $this->assertEquals('company.org', $email->getProvider()); $this->assertEquals('mail.sub', $email->getSubdomain()); $this->assertEquals(true, $email->hasSubdomain()); } public function test_email_formatted(): void { - $email = new Email('user@mail.example.com'); + $email = new Email('user@mail.company.org'); - $this->assertEquals('user@mail.example.com', $email->getFormatted('full')); + $this->assertEquals('user@mail.company.org', $email->getFormatted('full')); $this->assertEquals('user', $email->getFormatted('local')); - $this->assertEquals('mail.example.com', $email->getFormatted('domain')); - $this->assertEquals('example.com', $email->getFormatted('provider')); + $this->assertEquals('mail.company.org', $email->getFormatted('domain')); + $this->assertEquals('company.org', $email->getFormatted('provider')); $this->assertEquals('mail', $email->getFormatted('subdomain')); } public function test_email_normalization(): void { - $email = new Email(' USER@EXAMPLE.COM '); + $email = new Email(' USER@COMPANY.ORG '); - $this->assertEquals('user@example.com', $email->get()); - $this->assertEquals('user@example.com', $email->normalize()); + $this->assertEquals('user@company.org', $email->get()); + $this->assertEquals('user@company.org', $email->normalize()); } public function test_invalid_email_empty(): void diff --git a/tests/Validator/EmailCorporateTest.php b/tests/Validator/EmailCorporateTest.php index 72aeb82..5af4288 100644 --- a/tests/Validator/EmailCorporateTest.php +++ b/tests/Validator/EmailCorporateTest.php @@ -71,10 +71,10 @@ public function test_invalid_disposable_email(): void $this->assertEquals(false, $validator->isValid('user@maildrop.cc')); $this->assertEquals(false, $validator->isValid('user@sharklasers.com')); $this->assertEquals(false, $validator->isValid('user@test.com')); - // example.com is no longer considered disposable, so it's corporate - $this->assertEquals(true, $validator->isValid('user@example.com')); - $this->assertEquals(true, $validator->isValid('user@example.org')); - $this->assertEquals(true, $validator->isValid('user@example.net')); + // company.org is corporate + $this->assertEquals(true, $validator->isValid('user@company.org')); + $this->assertEquals(true, $validator->isValid('user@business.org')); + $this->assertEquals(true, $validator->isValid('user@enterprise.net')); } public function test_invalid_email_format(): void diff --git a/tests/Validator/EmailNotDisposableTest.php b/tests/Validator/EmailNotDisposableTest.php index ef8ee6f..600e9dd 100644 --- a/tests/Validator/EmailNotDisposableTest.php +++ b/tests/Validator/EmailNotDisposableTest.php @@ -24,7 +24,7 @@ public function test_valid_non_disposable_email(): void { $validator = new EmailNotDisposable; - $this->assertEquals(true, $validator->isValid('test@example.com')); + $this->assertEquals(true, $validator->isValid('test@company.org')); $this->assertEquals(true, $validator->isValid('user@gmail.com')); $this->assertEquals(true, $validator->isValid('user@yahoo.com')); $this->assertEquals(true, $validator->isValid('user@company.com')); @@ -46,10 +46,10 @@ public function test_invalid_disposable_email(): void $this->assertEquals(false, $validator->isValid('user@maildrop.cc')); $this->assertEquals(false, $validator->isValid('user@sharklasers.com')); $this->assertEquals(false, $validator->isValid('user@test.com')); - // example.com is no longer considered disposable - $this->assertEquals(true, $validator->isValid('user@example.com')); - $this->assertEquals(true, $validator->isValid('user@example.org')); - $this->assertEquals(true, $validator->isValid('user@example.net')); + // company.org is not disposable + $this->assertEquals(true, $validator->isValid('user@company.org')); + $this->assertEquals(true, $validator->isValid('user@business.org')); + $this->assertEquals(true, $validator->isValid('user@enterprise.net')); } public function test_invalid_email_format(): void From 60205566a3af4198b4dff3858d0045751849a1de Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 18 Oct 2025 23:08:54 +0100 Subject: [PATCH 07/21] Remove unnecessary whitespace in Email.php for cleaner code formatting --- src/Emails/Email.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Emails/Email.php b/src/Emails/Email.php index aaad7da..4933e75 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -215,7 +215,7 @@ public function isCorporate(): bool if ($this->isFree() && $this->isDisposable()) { return false; // It's free, not corporate } - + return ! $this->isFree() && ! $this->isDisposable(); } From 73f5345b1222e46bfee8b04d891cd3f9ed917032 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 18 Oct 2025 23:20:21 +0100 Subject: [PATCH 08/21] Update GitHub Actions workflows to use actions/checkout@v4 for improved performance and consistency --- .github/workflows/linter.yml | 6 +----- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index e27f301..1139d1c 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -8,11 +8,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 2 - - - run: git checkout HEAD^2 + uses: actions/checkout@v4 - name: Run Linter run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9dc5183..488d148 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: "Tests" on: [pull_request] jobs: - lint: + test: name: Tests ${{ matrix.php-versions }} runs-on: ubuntu-latest strategy: @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP ${{ matrix.php-versions }} uses: shivammathur/setup-php@v2 From 0a6a373e3978b63346fd4a3a2e73b205d7e9c7a6 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 09:18:46 +0100 Subject: [PATCH 09/21] Refactor Email class to support unique email normalization and add provider-specific handling; remove deprecated sources.php file and update README badges for CI workflows. --- README.md | 4 +- composer.json | 4 - data/sources.php | 67 --- src/Emails/Email.php | 102 ++++ src/Emails/Normalizer/Provider.php | 72 +++ src/Emails/Normalizer/Providers/Fastmail.php | 57 +++ src/Emails/Normalizer/Providers/Generic.php | 56 +++ src/Emails/Normalizer/Providers/Gmail.php | 52 ++ src/Emails/Normalizer/Providers/Icloud.php | 57 +++ src/Emails/Normalizer/Providers/Outlook.php | 52 ++ .../Normalizer/Providers/Protonmail.php | 57 +++ src/Emails/Normalizer/Providers/Yahoo.php | 64 +++ tests/EmailTest.php | 457 ++++++++++++++++++ tests/Normalizer/Providers/FastmailTest.php | 83 ++++ tests/Normalizer/Providers/GenericTest.php | 93 ++++ tests/Normalizer/Providers/GmailTest.php | 84 ++++ tests/Normalizer/Providers/IcloudTest.php | 85 ++++ tests/Normalizer/Providers/OutlookTest.php | 95 ++++ tests/Normalizer/Providers/ProtonmailTest.php | 85 ++++ tests/Normalizer/Providers/YahooTest.php | 104 ++++ 20 files changed, 1658 insertions(+), 72 deletions(-) delete mode 100644 data/sources.php create mode 100644 src/Emails/Normalizer/Provider.php create mode 100644 src/Emails/Normalizer/Providers/Fastmail.php create mode 100644 src/Emails/Normalizer/Providers/Generic.php create mode 100644 src/Emails/Normalizer/Providers/Gmail.php create mode 100644 src/Emails/Normalizer/Providers/Icloud.php create mode 100644 src/Emails/Normalizer/Providers/Outlook.php create mode 100644 src/Emails/Normalizer/Providers/Protonmail.php create mode 100644 src/Emails/Normalizer/Providers/Yahoo.php create mode 100644 tests/Normalizer/Providers/FastmailTest.php create mode 100644 tests/Normalizer/Providers/GenericTest.php create mode 100644 tests/Normalizer/Providers/GmailTest.php create mode 100644 tests/Normalizer/Providers/IcloudTest.php create mode 100644 tests/Normalizer/Providers/OutlookTest.php create mode 100644 tests/Normalizer/Providers/ProtonmailTest.php create mode 100644 tests/Normalizer/Providers/YahooTest.php diff --git a/README.md b/README.md index ebd0b85..1f657df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Utopia Emails -[![Build Status](https://travis-ci.org/utopia-php/emails.svg?branch=master)](https://travis-ci.com/utopia-php/emails) +[![Tests](https://github.com/utopia-php/emails/workflows/Tests/badge.svg)](https://github.com/utopia-php/emails/actions/workflows/test.yml) +[![Linter](https://github.com/utopia-php/emails/workflows/Linter/badge.svg)](https://github.com/utopia-php/emails/actions/workflows/linter.yml) +[![CodeQL](https://github.com/utopia-php/emails/workflows/CodeQL/badge.svg)](https://github.com/utopia-php/emails/actions/workflows/codeql-analysis.yml) ![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/emails.svg) [![Discord](https://img.shields.io/discord/564160730845151244)](https://appwrite.io/discord) diff --git a/composer.json b/composer.json index dc9dc37..c27cdbf 100644 --- a/composer.json +++ b/composer.json @@ -8,10 +8,6 @@ { "name": "Eldad Fux", "email": "eldad@appwrite.io" - }, - { - "name": "Wess Cope", - "email": "wess@appwrite.io" } ], "autoload": { diff --git a/data/sources.php b/data/sources.php deleted file mode 100644 index fe5e715..0000000 --- a/data/sources.php +++ /dev/null @@ -1,67 +0,0 @@ - [ - 'manual' => [ - 'name' => 'Manual Disposable Email Domains', - 'url' => null, - 'enabled' => true, - 'description' => 'Manually managed disposable email domains', - 'configFile' => 'disposable-domains-manual.php', - ], - 'martenson' => [ - 'name' => 'Martenson Disposable Email Domains', - 'url' => 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf', - 'enabled' => true, - 'description' => 'Comprehensive list of disposable email domains from Martenson repository', - ], - 'disposable' => [ - 'name' => 'Disposable Email Domains', - 'url' => 'https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt', - 'enabled' => true, - 'description' => 'Popular disposable email domains list', - ], - 'wesbos' => [ - 'name' => 'Wes Bos Burner Email Providers', - 'url' => 'https://raw.githubusercontent.com/wesbos/burner-email-providers/refs/heads/master/emails.txt', - 'enabled' => true, - 'description' => 'Burner email providers from Wes Bos repository', - ], - 'fakefilter' => [ - 'name' => '7c FakeFilter Domains', - 'url' => 'https://raw.githubusercontent.com/7c/fakefilter/main/txt/data.txt', - 'enabled' => true, - 'description' => 'Fake email domains from 7c FakeFilter', - ], - 'adamloving' => [ - 'name' => 'Adam Loving Temporary Email Domains', - 'url' => 'https://gist.githubusercontent.com/adamloving/4401361/raw/e81212c3caecb54b87ced6392e0a0de2b6466287/temporary-email-address-domains', - 'enabled' => true, - 'description' => 'Temporary email domains from Adam Loving gist', - ], - ], - 'free' => [ - 'manual' => [ - 'name' => 'Manual Free Email Domains', - 'url' => null, - 'enabled' => true, - 'description' => 'Manually managed free email domains', - 'configFile' => 'free-domains-manual.php', - ], - 'kikobeats' => [ - 'name' => 'Kikobeats Free Email Domains', - 'url' => 'https://raw.githubusercontent.com/Kikobeats/free-email-domains/master/domains.json', - 'enabled' => true, - 'description' => 'Free email domains from Kikobeats repository', - ], - ], -]; diff --git a/src/Emails/Email.php b/src/Emails/Email.php index 4933e75..33aeae3 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -3,6 +3,14 @@ namespace Utopia\Emails; use Exception; +use Utopia\Emails\Normalizer\Provider; +use Utopia\Emails\Normalizer\Providers\Fastmail; +use Utopia\Emails\Normalizer\Providers\Generic; +use Utopia\Emails\Normalizer\Providers\Gmail; +use Utopia\Emails\Normalizer\Providers\Icloud; +use Utopia\Emails\Normalizer\Providers\Outlook; +use Utopia\Emails\Normalizer\Providers\Protonmail; +use Utopia\Emails\Normalizer\Providers\Yahoo; class Email { @@ -58,6 +66,13 @@ class Email */ protected static $disposableDomains = null; + /** + * Email providers + * + * @var Provider[] + */ + protected static $providers = null; + /** * Email constructor. */ @@ -268,6 +283,93 @@ public function normalize(): string return $this->email; } + /** + * Get unique email address by removing aliases and provider-specific variations + * This method removes plus addressing, dot notation (for Gmail), and other aliasing techniques + * to return the canonical form of the email address + */ + public function getUnique(): string + { + $provider = $this->getProviderForDomain($this->domain); + $normalized = $provider->normalize($this->local, $this->domain); + + return $normalized['local'].'@'.$normalized['domain']; + } + + /** + * Check if the email domain is supported for normalization + */ + public function isNormalizationSupported(): bool + { + return $this->isDomainSupported($this->domain); + } + + /** + * Get the canonical domain for this email + */ + public function getCanonicalDomain(): ?string + { + $provider = $this->getProviderForDomain($this->domain); + + // Only return canonical domain if it's not the generic provider + if (! $provider instanceof Generic) { + return $provider->getCanonicalDomain(); + } + + return null; + } + + /** + * Get the appropriate provider for a given domain + */ + protected function getProviderForDomain(string $domain): Provider + { + if (self::$providers === null) { + self::$providers = [ + new Gmail, + new Outlook, + new Yahoo, + new Icloud, + new Protonmail, + new Fastmail, + ]; + } + + foreach (self::$providers as $provider) { + if ($provider->supports($domain)) { + return $provider; + } + } + + // Return generic provider if no specific provider found + return new Generic; + } + + /** + * Check if a domain is supported by any provider + */ + protected function isDomainSupported(string $domain): bool + { + if (self::$providers === null) { + self::$providers = [ + new Gmail, + new Outlook, + new Yahoo, + new Icloud, + new Protonmail, + new Fastmail, + ]; + } + + foreach (self::$providers as $provider) { + if ($provider->supports($domain)) { + return true; + } + } + + return false; + } + /** * Get email in different formats */ diff --git a/src/Emails/Normalizer/Provider.php b/src/Emails/Normalizer/Provider.php new file mode 100644 index 0000000..0a4db15 --- /dev/null +++ b/src/Emails/Normalizer/Provider.php @@ -0,0 +1,72 @@ + 0) { + return substr($local, 0, $plusPos); + } + + return $local; + } + + /** + * Remove all dots from local part + */ + protected function removeDots(string $local): string + { + return str_replace('.', '', $local); + } + + /** + * Remove all hyphens from local part + */ + protected function removeHyphens(string $local): string + { + return str_replace('-', '', $local); + } + + /** + * Convert local part to lowercase + */ + protected function toLowerCase(string $local): string + { + return strtolower($local); + } +} diff --git a/src/Emails/Normalizer/Providers/Fastmail.php b/src/Emails/Normalizer/Providers/Fastmail.php new file mode 100644 index 0000000..75082ef --- /dev/null +++ b/src/Emails/Normalizer/Providers/Fastmail.php @@ -0,0 +1,57 @@ +toLowerCase($local); + + // Check if there's plus addressing + $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + + // Remove plus addressing (everything after +) + $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + + // Remove dots only if there was plus addressing (Fastmail treats dots as aliases only with plus) + if ($hasPlus) { + $normalizedLocal = $this->removeDots($normalizedLocal); + } + + return [ + 'local' => $normalizedLocal, + 'domain' => self::CANONICAL_DOMAIN, + ]; + } + + public function getCanonicalDomain(): string + { + return self::CANONICAL_DOMAIN; + } + + public function getSupportedDomains(): array + { + return self::SUPPORTED_DOMAINS; + } +} diff --git a/src/Emails/Normalizer/Providers/Generic.php b/src/Emails/Normalizer/Providers/Generic.php new file mode 100644 index 0000000..9f888f8 --- /dev/null +++ b/src/Emails/Normalizer/Providers/Generic.php @@ -0,0 +1,56 @@ +toLowerCase($local); + + // Check if there's plus addressing + $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + + // Remove plus addressing (everything after +) + $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + + // Remove dots and hyphens only if there was plus addressing (generic providers treat these as aliases only with plus) + if ($hasPlus) { + $normalizedLocal = $this->removeDots($normalizedLocal); + $normalizedLocal = $this->removeHyphens($normalizedLocal); + } + + return [ + 'local' => $normalizedLocal, + 'domain' => $domain, + ]; + } + + public function getCanonicalDomain(): string + { + // Generic provider doesn't have a canonical domain + return ''; + } + + public function getSupportedDomains(): array + { + // Generic provider supports all domains + return []; + } +} diff --git a/src/Emails/Normalizer/Providers/Gmail.php b/src/Emails/Normalizer/Providers/Gmail.php new file mode 100644 index 0000000..ad31e69 --- /dev/null +++ b/src/Emails/Normalizer/Providers/Gmail.php @@ -0,0 +1,52 @@ +toLowerCase($local); + + // Remove all dots from local part + $normalizedLocal = $this->removeDots($normalizedLocal); + + // Remove plus addressing (everything after +) + $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + + return [ + 'local' => $normalizedLocal, + 'domain' => self::CANONICAL_DOMAIN, + ]; + } + + public function getCanonicalDomain(): string + { + return self::CANONICAL_DOMAIN; + } + + public function getSupportedDomains(): array + { + return self::SUPPORTED_DOMAINS; + } +} diff --git a/src/Emails/Normalizer/Providers/Icloud.php b/src/Emails/Normalizer/Providers/Icloud.php new file mode 100644 index 0000000..61c316b --- /dev/null +++ b/src/Emails/Normalizer/Providers/Icloud.php @@ -0,0 +1,57 @@ +toLowerCase($local); + + // Check if there's plus addressing + $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + + // Remove plus addressing (everything after +) + $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + + // Remove dots only if there was plus addressing (iCloud treats dots as aliases only with plus) + if ($hasPlus) { + $normalizedLocal = $this->removeDots($normalizedLocal); + } + + return [ + 'local' => $normalizedLocal, + 'domain' => self::CANONICAL_DOMAIN, + ]; + } + + public function getCanonicalDomain(): string + { + return self::CANONICAL_DOMAIN; + } + + public function getSupportedDomains(): array + { + return self::SUPPORTED_DOMAINS; + } +} diff --git a/src/Emails/Normalizer/Providers/Outlook.php b/src/Emails/Normalizer/Providers/Outlook.php new file mode 100644 index 0000000..0eac594 --- /dev/null +++ b/src/Emails/Normalizer/Providers/Outlook.php @@ -0,0 +1,52 @@ +toLowerCase($local); + + // Remove plus addressing (everything after +) + $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + + return [ + 'local' => $normalizedLocal, + 'domain' => self::CANONICAL_DOMAIN, + ]; + } + + public function getCanonicalDomain(): string + { + return self::CANONICAL_DOMAIN; + } + + public function getSupportedDomains(): array + { + return self::SUPPORTED_DOMAINS; + } +} diff --git a/src/Emails/Normalizer/Providers/Protonmail.php b/src/Emails/Normalizer/Providers/Protonmail.php new file mode 100644 index 0000000..dc6ff79 --- /dev/null +++ b/src/Emails/Normalizer/Providers/Protonmail.php @@ -0,0 +1,57 @@ +toLowerCase($local); + + // Check if there's plus addressing + $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + + // Remove plus addressing (everything after +) + $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + + // Remove dots only if there was plus addressing (ProtonMail treats dots as aliases only with plus) + if ($hasPlus) { + $normalizedLocal = $this->removeDots($normalizedLocal); + } + + return [ + 'local' => $normalizedLocal, + 'domain' => self::CANONICAL_DOMAIN, + ]; + } + + public function getCanonicalDomain(): string + { + return self::CANONICAL_DOMAIN; + } + + public function getSupportedDomains(): array + { + return self::SUPPORTED_DOMAINS; + } +} diff --git a/src/Emails/Normalizer/Providers/Yahoo.php b/src/Emails/Normalizer/Providers/Yahoo.php new file mode 100644 index 0000000..d3308f5 --- /dev/null +++ b/src/Emails/Normalizer/Providers/Yahoo.php @@ -0,0 +1,64 @@ +toLowerCase($local); + + // Check if there's plus addressing + $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + + // Remove plus addressing (everything after +) + $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + + // Remove dots only if there was plus addressing (Yahoo treats dots as aliases only with plus) + if ($hasPlus) { + $normalizedLocal = $this->removeDots($normalizedLocal); + } + + // Remove hyphens (Yahoo treats hyphens as aliases) + $normalizedLocal = $this->removeHyphens($normalizedLocal); + + return [ + 'local' => $normalizedLocal, + 'domain' => self::CANONICAL_DOMAIN, + ]; + } + + public function getCanonicalDomain(): string + { + return self::CANONICAL_DOMAIN; + } + + public function getSupportedDomains(): array + { + return self::SUPPORTED_DOMAINS; + } +} diff --git a/tests/EmailTest.php b/tests/EmailTest.php index c8f4bbd..9de2366 100644 --- a/tests/EmailTest.php +++ b/tests/EmailTest.php @@ -382,4 +382,461 @@ public function test_corporate_email_providers(): void $this->assertEquals(true, $email->isCorporate(), "Failed for provider: {$provider}"); } } + + public function test_get_unique_gmail_aliases(): void + { + $testCases = [ + // Gmail dot notation and plus addressing + ['user.name@gmail.com', 'username@gmail.com'], + ['user.name+tag@gmail.com', 'username@gmail.com'], + ['user.name+spam@gmail.com', 'username@gmail.com'], + ['user.name+newsletter@gmail.com', 'username@gmail.com'], + ['user.name+work@gmail.com', 'username@gmail.com'], + ['user.name+personal@gmail.com', 'username@gmail.com'], + ['user.name+test123@gmail.com', 'username@gmail.com'], + ['user.name+anything@gmail.com', 'username@gmail.com'], + ['user.name+verylongtag@gmail.com', 'username@gmail.com'], + ['user.name+tag.with.dots@gmail.com', 'username@gmail.com'], + ['user.name+tag-with-hyphens@gmail.com', 'username@gmail.com'], + ['user.name+tag_with_underscores@gmail.com', 'username@gmail.com'], + ['user.name+tag123@gmail.com', 'username@gmail.com'], + ['user.name+tag@googlemail.com', 'username@gmail.com'], + ['user.name+tag@googlemail.com', 'username@gmail.com'], + ['user.name+spam@googlemail.com', 'username@gmail.com'], + ['user.name@googlemail.com', 'username@gmail.com'], + // Multiple dots + ['u.s.e.r.n.a.m.e@gmail.com', 'username@gmail.com'], + ['u.s.e.r.n.a.m.e+tag@gmail.com', 'username@gmail.com'], + // Edge cases + ['user+@gmail.com', 'user@gmail.com'], + ['user.@gmail.com', 'user@gmail.com'], + ['.user@gmail.com', 'user@gmail.com'], + ['user..name@gmail.com', 'username@gmail.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_outlook_aliases(): void + { + $testCases = [ + // Outlook/Hotmail/Live plus addressing + ['user.name+tag@outlook.com', 'user.name@outlook.com'], + ['user.name+spam@outlook.com', 'user.name@outlook.com'], + ['user.name+newsletter@outlook.com', 'user.name@outlook.com'], + ['user.name+work@outlook.com', 'user.name@outlook.com'], + ['user.name+personal@outlook.com', 'user.name@outlook.com'], + ['user.name+test123@outlook.com', 'user.name@outlook.com'], + ['user.name+anything@outlook.com', 'user.name@outlook.com'], + ['user.name+verylongtag@outlook.com', 'user.name@outlook.com'], + ['user.name+tag.with.dots@outlook.com', 'user.name@outlook.com'], + ['user.name+tag-with-hyphens@outlook.com', 'user.name@outlook.com'], + ['user.name+tag_with_underscores@outlook.com', 'user.name@outlook.com'], + ['user.name+tag123@outlook.com', 'user.name@outlook.com'], + // Hotmail + ['user.name+tag@hotmail.com', 'user.name@outlook.com'], + ['user.name+spam@hotmail.com', 'user.name@outlook.com'], + ['user.name@hotmail.com', 'user.name@outlook.com'], + // Live + ['user.name+tag@live.com', 'user.name@outlook.com'], + ['user.name+spam@live.com', 'user.name@outlook.com'], + ['user.name@live.com', 'user.name@outlook.com'], + // UK variants + ['user.name+tag@outlook.co.uk', 'user.name@outlook.com'], + ['user.name+tag@hotmail.co.uk', 'user.name@outlook.com'], + ['user.name+tag@live.co.uk', 'user.name@outlook.com'], + // Dots are preserved for Outlook + ['user.name@outlook.com', 'user.name@outlook.com'], + ['u.s.e.r.n.a.m.e@outlook.com', 'u.s.e.r.n.a.m.e@outlook.com'], + // Edge cases + ['user+@outlook.com', 'user@outlook.com'], + ['user.@outlook.com', 'user.@outlook.com'], + ['.user@outlook.com', '.user@outlook.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_yahoo_aliases(): void + { + $testCases = [ + // Yahoo plus addressing and hyphen removal + ['user.name+tag@yahoo.com', 'username@yahoo.com'], + ['user.name+spam@yahoo.com', 'username@yahoo.com'], + ['user.name+newsletter@yahoo.com', 'username@yahoo.com'], + ['user.name+work@yahoo.com', 'username@yahoo.com'], + ['user.name+personal@yahoo.com', 'username@yahoo.com'], + ['user.name+test123@yahoo.com', 'username@yahoo.com'], + ['user.name+anything@yahoo.com', 'username@yahoo.com'], + ['user.name+verylongtag@yahoo.com', 'username@yahoo.com'], + ['user.name+tag.with.dots@yahoo.com', 'username@yahoo.com'], + ['user.name+tag-with-hyphens@yahoo.com', 'username@yahoo.com'], + ['user.name+tag_with_underscores@yahoo.com', 'username@yahoo.com'], + ['user.name+tag123@yahoo.com', 'username@yahoo.com'], + // Hyphen removal + ['user-name@yahoo.com', 'username@yahoo.com'], + ['user-name+tag@yahoo.com', 'username@yahoo.com'], + ['user-name+spam@yahoo.com', 'username@yahoo.com'], + ['user-name+newsletter@yahoo.com', 'username@yahoo.com'], + ['user-name+work@yahoo.com', 'username@yahoo.com'], + ['user-name+personal@yahoo.com', 'username@yahoo.com'], + ['user-name+test123@yahoo.com', 'username@yahoo.com'], + ['user-name+anything@yahoo.com', 'username@yahoo.com'], + ['user-name+verylongtag@yahoo.com', 'username@yahoo.com'], + ['user-name+tag.with.dots@yahoo.com', 'username@yahoo.com'], + ['user-name+tag-with-hyphens@yahoo.com', 'username@yahoo.com'], + ['user-name+tag_with_underscores@yahoo.com', 'username@yahoo.com'], + ['user-name+tag123@yahoo.com', 'username@yahoo.com'], + // Multiple hyphens + ['u-s-e-r-n-a-m-e@yahoo.com', 'username@yahoo.com'], + ['u-s-e-r-n-a-m-e+tag@yahoo.com', 'username@yahoo.com'], + // Other Yahoo domains + ['user.name+tag@yahoo.co.uk', 'username@yahoo.com'], + ['user.name+tag@yahoo.ca', 'username@yahoo.com'], + ['user.name+tag@ymail.com', 'username@yahoo.com'], + ['user.name+tag@rocketmail.com', 'username@yahoo.com'], + // Edge cases + ['user+@yahoo.com', 'user@yahoo.com'], + ['user-@yahoo.com', 'user@yahoo.com'], + ['user.@yahoo.com', 'user.@yahoo.com'], + ['.user@yahoo.com', '.user@yahoo.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_icloud_aliases(): void + { + $testCases = [ + // iCloud plus addressing + ['user.name+tag@icloud.com', 'username@icloud.com'], + ['user.name+spam@icloud.com', 'username@icloud.com'], + ['user.name+newsletter@icloud.com', 'username@icloud.com'], + ['user.name+work@icloud.com', 'username@icloud.com'], + ['user.name+personal@icloud.com', 'username@icloud.com'], + ['user.name+test123@icloud.com', 'username@icloud.com'], + ['user.name+anything@icloud.com', 'username@icloud.com'], + ['user.name+verylongtag@icloud.com', 'username@icloud.com'], + ['user.name+tag.with.dots@icloud.com', 'username@icloud.com'], + ['user.name+tag-with-hyphens@icloud.com', 'username@icloud.com'], + ['user.name+tag_with_underscores@icloud.com', 'username@icloud.com'], + ['user.name+tag123@icloud.com', 'username@icloud.com'], + // Other Apple domains + ['user.name+tag@me.com', 'username@icloud.com'], + ['user.name+tag@mac.com', 'username@icloud.com'], + // Dots are preserved for iCloud + ['user.name@icloud.com', 'user.name@icloud.com'], + ['u.s.e.r.n.a.m.e@icloud.com', 'u.s.e.r.n.a.m.e@icloud.com'], + // Edge cases + ['user+@icloud.com', 'user@icloud.com'], + ['user.@icloud.com', 'user.@icloud.com'], + ['.user@icloud.com', '.user@icloud.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_protonmail_aliases(): void + { + $testCases = [ + // ProtonMail plus addressing + ['user.name+tag@protonmail.com', 'username@protonmail.com'], + ['user.name+spam@protonmail.com', 'username@protonmail.com'], + ['user.name+newsletter@protonmail.com', 'username@protonmail.com'], + ['user.name+work@protonmail.com', 'username@protonmail.com'], + ['user.name+personal@protonmail.com', 'username@protonmail.com'], + ['user.name+test123@protonmail.com', 'username@protonmail.com'], + ['user.name+anything@protonmail.com', 'username@protonmail.com'], + ['user.name+verylongtag@protonmail.com', 'username@protonmail.com'], + ['user.name+tag.with.dots@protonmail.com', 'username@protonmail.com'], + ['user.name+tag-with-hyphens@protonmail.com', 'username@protonmail.com'], + ['user.name+tag_with_underscores@protonmail.com', 'username@protonmail.com'], + ['user.name+tag123@protonmail.com', 'username@protonmail.com'], + // Other ProtonMail domains + ['user.name+tag@proton.me', 'username@protonmail.com'], + ['user.name+tag@pm.me', 'username@protonmail.com'], + // Dots are preserved for ProtonMail + ['user.name@protonmail.com', 'user.name@protonmail.com'], + ['u.s.e.r.n.a.m.e@protonmail.com', 'u.s.e.r.n.a.m.e@protonmail.com'], + // Edge cases + ['user+@protonmail.com', 'user@protonmail.com'], + ['user.@protonmail.com', 'user.@protonmail.com'], + ['.user@protonmail.com', '.user@protonmail.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_fastmail_aliases(): void + { + $testCases = [ + // Fastmail plus addressing + ['user.name+tag@fastmail.com', 'username@fastmail.com'], + ['user.name+spam@fastmail.com', 'username@fastmail.com'], + ['user.name+newsletter@fastmail.com', 'username@fastmail.com'], + ['user.name+work@fastmail.com', 'username@fastmail.com'], + ['user.name+personal@fastmail.com', 'username@fastmail.com'], + ['user.name+test123@fastmail.com', 'username@fastmail.com'], + ['user.name+anything@fastmail.com', 'username@fastmail.com'], + ['user.name+verylongtag@fastmail.com', 'username@fastmail.com'], + ['user.name+tag.with.dots@fastmail.com', 'username@fastmail.com'], + ['user.name+tag-with-hyphens@fastmail.com', 'username@fastmail.com'], + ['user.name+tag_with_underscores@fastmail.com', 'username@fastmail.com'], + ['user.name+tag123@fastmail.com', 'username@fastmail.com'], + // Other Fastmail domain + ['user.name+tag@fastmail.fm', 'username@fastmail.com'], + // Dots are preserved for Fastmail + ['user.name@fastmail.com', 'user.name@fastmail.com'], + ['u.s.e.r.n.a.m.e@fastmail.com', 'u.s.e.r.n.a.m.e@fastmail.com'], + // Edge cases + ['user+@fastmail.com', 'user@fastmail.com'], + ['user.@fastmail.com', 'user.@fastmail.com'], + ['.user@fastmail.com', '.user@fastmail.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_other_domains(): void + { + $testCases = [ + // Other domains with plus addressing + ['user.name+tag@example.com', 'username@example.com'], + ['user.name+spam@example.com', 'username@example.com'], + ['user.name+newsletter@example.com', 'username@example.com'], + ['user.name+work@example.com', 'username@example.com'], + ['user.name+personal@example.com', 'username@example.com'], + ['user.name+test123@example.com', 'username@example.com'], + ['user.name+anything@example.com', 'username@example.com'], + ['user.name+verylongtag@example.com', 'username@example.com'], + ['user.name+tag.with.dots@example.com', 'username@example.com'], + ['user.name+tag-with-hyphens@example.com', 'username@example.com'], + ['user.name+tag_with_underscores@example.com', 'username@example.com'], + ['user.name+tag123@example.com', 'username@example.com'], + // Dots are preserved for other domains + ['user.name@example.com', 'user.name@example.com'], + ['u.s.e.r.n.a.m.e@example.com', 'u.s.e.r.n.a.m.e@example.com'], + // Hyphens are preserved for other domains + ['user-name@example.com', 'user-name@example.com'], + ['user-name+tag@example.com', 'username@example.com'], + // Edge cases + ['user+@example.com', 'user@example.com'], + ['user.@example.com', 'user.@example.com'], + ['.user@example.com', '.user@example.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_edge_cases(): void + { + $testCases = [ + // Empty plus addressing + ['user+@gmail.com', 'user@gmail.com'], + ['user+@outlook.com', 'user@outlook.com'], + ['user+@yahoo.com', 'user@yahoo.com'], + ['user+@icloud.com', 'user@icloud.com'], + ['user+@protonmail.com', 'user@protonmail.com'], + ['user+@fastmail.com', 'user@fastmail.com'], + ['user+@example.com', 'user@example.com'], + // Plus at the beginning + ['+user@gmail.com', '+user@gmail.com'], + ['+user@outlook.com', '+user@outlook.com'], + ['+user@yahoo.com', '+user@yahoo.com'], + ['+user@icloud.com', '+user@icloud.com'], + ['+user@protonmail.com', '+user@protonmail.com'], + ['+user@fastmail.com', '+user@fastmail.com'], + ['+user@example.com', '+user@example.com'], + // Multiple plus signs (only first one is considered) + ['user+tag+more@gmail.com', 'user@gmail.com'], + ['user+tag+more@outlook.com', 'user@outlook.com'], + ['user+tag+more@yahoo.com', 'user@yahoo.com'], + ['user+tag+more@icloud.com', 'user@icloud.com'], + ['user+tag+more@protonmail.com', 'user@protonmail.com'], + ['user+tag+more@fastmail.com', 'user@fastmail.com'], + ['user+tag+more@example.com', 'user@example.com'], + // Special characters in plus addressing + ['user+tag!@gmail.com', 'user@gmail.com'], + ['user+tag#@gmail.com', 'user@gmail.com'], + ['user+tag$@gmail.com', 'user@gmail.com'], + ['user+tag%@gmail.com', 'user@gmail.com'], + ['user+tag&@gmail.com', 'user@gmail.com'], + ['user+tag*@gmail.com', 'user@gmail.com'], + ['user+tag(@gmail.com', 'user@gmail.com'], + ['user+tag)@gmail.com', 'user@gmail.com'], + ['user+tag=@gmail.com', 'user@gmail.com'], + ['user+tag[@gmail.com', 'user@gmail.com'], + ['user+tag]@gmail.com', 'user@gmail.com'], + ['user+tag{@gmail.com', 'user@gmail.com'], + ['user+tag}@gmail.com', 'user@gmail.com'], + ['user+tag|@gmail.com', 'user@gmail.com'], + ['user+tag\@gmail.com', 'user@gmail.com'], + ['user+tag/@gmail.com', 'user@gmail.com'], + ['user+tag?@gmail.com', 'user@gmail.com'], + ['user+tag<@gmail.com', 'user@gmail.com'], + ['user+tag>@gmail.com', 'user@gmail.com'], + ['user+tag,@gmail.com', 'user@gmail.com'], + ['user+tag;@gmail.com', 'user@gmail.com'], + ['user+tag:@gmail.com', 'user@gmail.com'], + ['user+tag"@gmail.com', 'user@gmail.com'], + ['user+tag\'@gmail.com', 'user@gmail.com'], + ['user+tag~@gmail.com', 'user@gmail.com'], + ['user+tag`@gmail.com', 'user@gmail.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_get_unique_case_sensitivity(): void + { + $testCases = [ + // Case sensitivity should not matter + ['USER.NAME+TAG@GMAIL.COM', 'username@gmail.com'], + ['User.Name+Tag@Gmail.Com', 'username@gmail.com'], + ['user.name+tag@Gmail.com', 'username@gmail.com'], + ['USER.NAME+TAG@OUTLOOK.COM', 'user.name@outlook.com'], + ['User.Name+Tag@Outlook.Com', 'user.name@outlook.com'], + ['user.name+tag@Outlook.com', 'user.name@outlook.com'], + ['USER.NAME+TAG@YAHOO.COM', 'username@yahoo.com'], + ['User.Name+Tag@Yahoo.Com', 'username@yahoo.com'], + ['user.name+tag@Yahoo.com', 'username@yahoo.com'], + ['USER.NAME+TAG@ICLOUD.COM', 'username@icloud.com'], + ['User.Name+Tag@Icloud.Com', 'username@icloud.com'], + ['user.name+tag@Icloud.com', 'username@icloud.com'], + ['USER.NAME+TAG@PROTONMAIL.COM', 'username@protonmail.com'], + ['User.Name+Tag@Protonmail.Com', 'username@protonmail.com'], + ['user.name+tag@Protonmail.com', 'username@protonmail.com'], + ['USER.NAME+TAG@FASTMAIL.COM', 'username@fastmail.com'], + ['User.Name+Tag@Fastmail.Com', 'username@fastmail.com'], + ['user.name+tag@Fastmail.com', 'username@fastmail.com'], + ['USER.NAME+TAG@EXAMPLE.COM', 'username@example.com'], + ['User.Name+Tag@Example.Com', 'username@example.com'], + ['user.name+tag@Example.com', 'username@example.com'], + ]; + + foreach ($testCases as [$input, $expected]) { + $email = new Email($input); + $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + } + } + + public function test_is_normalization_supported(): void + { + $supportedEmails = [ + 'user@gmail.com', + 'user@googlemail.com', + 'user@outlook.com', + 'user@hotmail.com', + 'user@live.com', + 'user@outlook.co.uk', + 'user@hotmail.co.uk', + 'user@live.co.uk', + 'user@yahoo.com', + 'user@yahoo.co.uk', + 'user@yahoo.ca', + 'user@ymail.com', + 'user@rocketmail.com', + 'user@icloud.com', + 'user@me.com', + 'user@mac.com', + 'user@protonmail.com', + 'user@proton.me', + 'user@pm.me', + 'user@fastmail.com', + 'user@fastmail.fm', + ]; + + foreach ($supportedEmails as $emailAddress) { + $email = new Email($emailAddress); + $this->assertTrue($email->isNormalizationSupported(), "Email {$emailAddress} should support normalization"); + } + + $unsupportedEmails = [ + 'user@example.com', + 'user@test.org', + 'user@company.net', + 'user@business.co.uk', + ]; + + foreach ($unsupportedEmails as $emailAddress) { + $email = new Email($emailAddress); + $this->assertFalse($email->isNormalizationSupported(), "Email {$emailAddress} should not support normalization"); + } + } + + public function test_get_canonical_domain(): void + { + $testCases = [ + ['user@gmail.com', 'gmail.com'], + ['user@googlemail.com', 'gmail.com'], + ['user@outlook.com', 'outlook.com'], + ['user@hotmail.com', 'outlook.com'], + ['user@live.com', 'outlook.com'], + ['user@outlook.co.uk', 'outlook.com'], + ['user@hotmail.co.uk', 'outlook.com'], + ['user@live.co.uk', 'outlook.com'], + ['user@yahoo.com', 'yahoo.com'], + ['user@yahoo.co.uk', 'yahoo.com'], + ['user@yahoo.ca', 'yahoo.com'], + ['user@ymail.com', 'yahoo.com'], + ['user@rocketmail.com', 'yahoo.com'], + ['user@icloud.com', 'icloud.com'], + ['user@me.com', 'icloud.com'], + ['user@mac.com', 'icloud.com'], + ['user@protonmail.com', 'protonmail.com'], + ['user@proton.me', 'protonmail.com'], + ['user@pm.me', 'protonmail.com'], + ['user@fastmail.com', 'fastmail.com'], + ['user@fastmail.fm', 'fastmail.com'], + ['user@example.com', null], + ['user@test.org', null], + ['user@company.net', null], + ['user@business.co.uk', null], + ]; + + foreach ($testCases as [$emailAddress, $expectedCanonical]) { + $email = new Email($emailAddress); + $this->assertEquals($expectedCanonical, $email->getCanonicalDomain(), "Failed for email: {$emailAddress}"); + } + } + + public function test_get_unique_with_different_providers(): void + { + // Test that different providers are used correctly + $gmailEmail = new Email('user.name+tag@gmail.com'); + $this->assertEquals('username@gmail.com', $gmailEmail->getUnique()); + + $outlookEmail = new Email('user.name+tag@outlook.com'); + $this->assertEquals('user.name@outlook.com', $outlookEmail->getUnique()); + + $yahooEmail = new Email('user-name+tag@yahoo.com'); + $this->assertEquals('username@yahoo.com', $yahooEmail->getUnique()); + + $genericEmail = new Email('user.name+tag@example.com'); + $this->assertEquals('username@example.com', $genericEmail->getUnique()); + } } diff --git a/tests/Normalizer/Providers/FastmailTest.php b/tests/Normalizer/Providers/FastmailTest.php new file mode 100644 index 0000000..b97527f --- /dev/null +++ b/tests/Normalizer/Providers/FastmailTest.php @@ -0,0 +1,83 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Normalizer\Providers; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Normalizer\Providers\Fastmail; + +class FastmailTest extends TestCase +{ + private Fastmail $provider; + + protected function setUp(): void + { + $this->provider = new Fastmail; + } + + public function test_supports(): void + { + $this->assertTrue($this->provider->supports('fastmail.com')); + $this->assertTrue($this->provider->supports('fastmail.fm')); + $this->assertFalse($this->provider->supports('gmail.com')); + $this->assertFalse($this->provider->supports('outlook.com')); + $this->assertFalse($this->provider->supports('example.com')); + } + + public function test_normalize(): void + { + $testCases = [ + ['user.name+tag', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+spam', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+newsletter', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+work', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+personal', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+test123', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+anything', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+verylongtag', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+tag.with.dots', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+tag-with-hyphens', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+tag_with_underscores', 'fastmail.com', 'username', 'fastmail.com'], + ['user.name+tag123', 'fastmail.com', 'username', 'fastmail.com'], + // Other Fastmail domain + ['user.name+tag', 'fastmail.fm', 'username', 'fastmail.com'], + // Dots are preserved for Fastmail + ['user.name', 'fastmail.com', 'user.name', 'fastmail.com'], + ['u.s.e.r.n.a.m.e', 'fastmail.com', 'u.s.e.r.n.a.m.e', 'fastmail.com'], + // Edge cases + ['user+', 'fastmail.com', 'user', 'fastmail.com'], + ['user.', 'fastmail.com', 'user.', 'fastmail.com'], + ['.user', 'fastmail.com', '.user', 'fastmail.com'], + ]; + + foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { + $result = $this->provider->normalize($inputLocal, $inputDomain); + $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); + $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); + } + } + + public function test_get_canonical_domain(): void + { + $this->assertEquals('fastmail.com', $this->provider->getCanonicalDomain()); + } + + public function test_get_supported_domains(): void + { + $domains = $this->provider->getSupportedDomains(); + $expected = ['fastmail.com', 'fastmail.fm']; + $this->assertEquals($expected, $domains); + } +} diff --git a/tests/Normalizer/Providers/GenericTest.php b/tests/Normalizer/Providers/GenericTest.php new file mode 100644 index 0000000..4856ff5 --- /dev/null +++ b/tests/Normalizer/Providers/GenericTest.php @@ -0,0 +1,93 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Normalizer\Providers; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Normalizer\Providers\Generic; + +class GenericTest extends TestCase +{ + private Generic $provider; + + protected function setUp(): void + { + $this->provider = new Generic; + } + + public function test_supports(): void + { + // Generic provider supports all domains + $this->assertTrue($this->provider->supports('example.com')); + $this->assertTrue($this->provider->supports('test.org')); + $this->assertTrue($this->provider->supports('company.net')); + $this->assertTrue($this->provider->supports('business.co.uk')); + $this->assertTrue($this->provider->supports('gmail.com')); + $this->assertTrue($this->provider->supports('outlook.com')); + $this->assertTrue($this->provider->supports('any-domain.com')); + } + + public function test_normalize(): void + { + $testCases = [ + // Other domains with plus addressing + ['user.name+tag', 'example.com', 'username', 'example.com'], + ['user.name+spam', 'example.com', 'username', 'example.com'], + ['user.name+newsletter', 'example.com', 'username', 'example.com'], + ['user.name+work', 'example.com', 'username', 'example.com'], + ['user.name+personal', 'example.com', 'username', 'example.com'], + ['user.name+test123', 'example.com', 'username', 'example.com'], + ['user.name+anything', 'example.com', 'username', 'example.com'], + ['user.name+verylongtag', 'example.com', 'username', 'example.com'], + ['user.name+tag.with.dots', 'example.com', 'username', 'example.com'], + ['user.name+tag-with-hyphens', 'example.com', 'username', 'example.com'], + ['user.name+tag_with_underscores', 'example.com', 'username', 'example.com'], + ['user.name+tag123', 'example.com', 'username', 'example.com'], + // Dots are preserved for other domains + ['user.name', 'example.com', 'user.name', 'example.com'], + ['u.s.e.r.n.a.m.e', 'example.com', 'u.s.e.r.n.a.m.e', 'example.com'], + // Hyphens are preserved for other domains + ['user-name', 'example.com', 'user-name', 'example.com'], + ['user-name+tag', 'example.com', 'username', 'example.com'], + // Edge cases + ['user+', 'example.com', 'user', 'example.com'], + ['user.', 'example.com', 'user.', 'example.com'], + ['.user', 'example.com', '.user', 'example.com'], + // Test with different domains + ['user.name+tag', 'test.org', 'username', 'test.org'], + ['user.name+tag', 'company.net', 'username', 'company.net'], + ['user.name+tag', 'business.co.uk', 'username', 'business.co.uk'], + ]; + + foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { + $result = $this->provider->normalize($inputLocal, $inputDomain); + $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); + $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); + } + } + + public function test_get_canonical_domain(): void + { + // Generic provider doesn't have a canonical domain + $this->assertEquals('', $this->provider->getCanonicalDomain()); + } + + public function test_get_supported_domains(): void + { + // Generic provider supports all domains + $domains = $this->provider->getSupportedDomains(); + $this->assertEquals([], $domains); + } +} diff --git a/tests/Normalizer/Providers/GmailTest.php b/tests/Normalizer/Providers/GmailTest.php new file mode 100644 index 0000000..a64e988 --- /dev/null +++ b/tests/Normalizer/Providers/GmailTest.php @@ -0,0 +1,84 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Normalizer\Providers; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Normalizer\Providers\Gmail; + +class GmailTest extends TestCase +{ + private Gmail $provider; + + protected function setUp(): void + { + $this->provider = new Gmail; + } + + public function test_supports(): void + { + $this->assertTrue($this->provider->supports('gmail.com')); + $this->assertTrue($this->provider->supports('googlemail.com')); + $this->assertFalse($this->provider->supports('outlook.com')); + $this->assertFalse($this->provider->supports('yahoo.com')); + $this->assertFalse($this->provider->supports('example.com')); + } + + public function test_normalize(): void + { + $testCases = [ + ['user.name', 'gmail.com', 'username', 'gmail.com'], + ['user.name+tag', 'gmail.com', 'username', 'gmail.com'], + ['user.name+spam', 'gmail.com', 'username', 'gmail.com'], + ['user.name+newsletter', 'gmail.com', 'username', 'gmail.com'], + ['user.name+work', 'gmail.com', 'username', 'gmail.com'], + ['user.name+personal', 'gmail.com', 'username', 'gmail.com'], + ['user.name+test123', 'gmail.com', 'username', 'gmail.com'], + ['user.name+anything', 'gmail.com', 'username', 'gmail.com'], + ['user.name+verylongtag', 'gmail.com', 'username', 'gmail.com'], + ['user.name+tag.with.dots', 'gmail.com', 'username', 'gmail.com'], + ['user.name+tag-with-hyphens', 'gmail.com', 'username', 'gmail.com'], + ['user.name+tag_with_underscores', 'gmail.com', 'username', 'gmail.com'], + ['user.name+tag123', 'gmail.com', 'username', 'gmail.com'], + ['u.s.e.r.n.a.m.e', 'gmail.com', 'username', 'gmail.com'], + ['u.s.e.r.n.a.m.e+tag', 'gmail.com', 'username', 'gmail.com'], + ['user+', 'gmail.com', 'user', 'gmail.com'], + ['user.', 'gmail.com', 'user', 'gmail.com'], + ['.user', 'gmail.com', 'user', 'gmail.com'], + ['user..name', 'gmail.com', 'username', 'gmail.com'], + // Googlemail domain + ['user.name+tag', 'googlemail.com', 'username', 'gmail.com'], + ['user.name+spam', 'googlemail.com', 'username', 'gmail.com'], + ['user.name', 'googlemail.com', 'username', 'gmail.com'], + ]; + + foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { + $result = $this->provider->normalize($inputLocal, $inputDomain); + $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); + $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); + } + } + + public function test_get_canonical_domain(): void + { + $this->assertEquals('gmail.com', $this->provider->getCanonicalDomain()); + } + + public function test_get_supported_domains(): void + { + $domains = $this->provider->getSupportedDomains(); + $this->assertEquals(['gmail.com', 'googlemail.com'], $domains); + } +} diff --git a/tests/Normalizer/Providers/IcloudTest.php b/tests/Normalizer/Providers/IcloudTest.php new file mode 100644 index 0000000..029d34b --- /dev/null +++ b/tests/Normalizer/Providers/IcloudTest.php @@ -0,0 +1,85 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Normalizer\Providers; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Normalizer\Providers\Icloud; + +class IcloudTest extends TestCase +{ + private Icloud $provider; + + protected function setUp(): void + { + $this->provider = new Icloud; + } + + public function test_supports(): void + { + $this->assertTrue($this->provider->supports('icloud.com')); + $this->assertTrue($this->provider->supports('me.com')); + $this->assertTrue($this->provider->supports('mac.com')); + $this->assertFalse($this->provider->supports('gmail.com')); + $this->assertFalse($this->provider->supports('outlook.com')); + $this->assertFalse($this->provider->supports('example.com')); + } + + public function test_normalize(): void + { + $testCases = [ + ['user.name+tag', 'icloud.com', 'username', 'icloud.com'], + ['user.name+spam', 'icloud.com', 'username', 'icloud.com'], + ['user.name+newsletter', 'icloud.com', 'username', 'icloud.com'], + ['user.name+work', 'icloud.com', 'username', 'icloud.com'], + ['user.name+personal', 'icloud.com', 'username', 'icloud.com'], + ['user.name+test123', 'icloud.com', 'username', 'icloud.com'], + ['user.name+anything', 'icloud.com', 'username', 'icloud.com'], + ['user.name+verylongtag', 'icloud.com', 'username', 'icloud.com'], + ['user.name+tag.with.dots', 'icloud.com', 'username', 'icloud.com'], + ['user.name+tag-with-hyphens', 'icloud.com', 'username', 'icloud.com'], + ['user.name+tag_with_underscores', 'icloud.com', 'username', 'icloud.com'], + ['user.name+tag123', 'icloud.com', 'username', 'icloud.com'], + // Other Apple domains + ['user.name+tag', 'me.com', 'username', 'icloud.com'], + ['user.name+tag', 'mac.com', 'username', 'icloud.com'], + // Dots are preserved for iCloud + ['user.name', 'icloud.com', 'user.name', 'icloud.com'], + ['u.s.e.r.n.a.m.e', 'icloud.com', 'u.s.e.r.n.a.m.e', 'icloud.com'], + // Edge cases + ['user+', 'icloud.com', 'user', 'icloud.com'], + ['user.', 'icloud.com', 'user.', 'icloud.com'], + ['.user', 'icloud.com', '.user', 'icloud.com'], + ]; + + foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { + $result = $this->provider->normalize($inputLocal, $inputDomain); + $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); + $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); + } + } + + public function test_get_canonical_domain(): void + { + $this->assertEquals('icloud.com', $this->provider->getCanonicalDomain()); + } + + public function test_get_supported_domains(): void + { + $domains = $this->provider->getSupportedDomains(); + $expected = ['icloud.com', 'me.com', 'mac.com']; + $this->assertEquals($expected, $domains); + } +} diff --git a/tests/Normalizer/Providers/OutlookTest.php b/tests/Normalizer/Providers/OutlookTest.php new file mode 100644 index 0000000..471bd1b --- /dev/null +++ b/tests/Normalizer/Providers/OutlookTest.php @@ -0,0 +1,95 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Normalizer\Providers; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Normalizer\Providers\Outlook; + +class OutlookTest extends TestCase +{ + private Outlook $provider; + + protected function setUp(): void + { + $this->provider = new Outlook; + } + + public function test_supports(): void + { + $this->assertTrue($this->provider->supports('outlook.com')); + $this->assertTrue($this->provider->supports('hotmail.com')); + $this->assertTrue($this->provider->supports('live.com')); + $this->assertTrue($this->provider->supports('outlook.co.uk')); + $this->assertTrue($this->provider->supports('hotmail.co.uk')); + $this->assertTrue($this->provider->supports('live.co.uk')); + $this->assertFalse($this->provider->supports('gmail.com')); + $this->assertFalse($this->provider->supports('yahoo.com')); + $this->assertFalse($this->provider->supports('example.com')); + } + + public function test_normalize(): void + { + $testCases = [ + ['user.name+tag', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+spam', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+newsletter', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+work', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+personal', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+test123', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+anything', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+verylongtag', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+tag.with.dots', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+tag-with-hyphens', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+tag_with_underscores', 'outlook.com', 'user.name', 'outlook.com'], + ['user.name+tag123', 'outlook.com', 'user.name', 'outlook.com'], + ['u.s.e.r.n.a.m.e', 'outlook.com', 'u.s.e.r.n.a.m.e', 'outlook.com'], + ['u.s.e.r.n.a.m.e+tag', 'outlook.com', 'u.s.e.r.n.a.m.e', 'outlook.com'], + ['user+', 'outlook.com', 'user', 'outlook.com'], + ['user.', 'outlook.com', 'user.', 'outlook.com'], + ['.user', 'outlook.com', '.user', 'outlook.com'], + // Hotmail + ['user.name+tag', 'hotmail.com', 'user.name', 'outlook.com'], + ['user.name+spam', 'hotmail.com', 'user.name', 'outlook.com'], + ['user.name', 'hotmail.com', 'user.name', 'outlook.com'], + // Live + ['user.name+tag', 'live.com', 'user.name', 'outlook.com'], + ['user.name+spam', 'live.com', 'user.name', 'outlook.com'], + ['user.name', 'live.com', 'user.name', 'outlook.com'], + // UK variants + ['user.name+tag', 'outlook.co.uk', 'user.name', 'outlook.com'], + ['user.name+tag', 'hotmail.co.uk', 'user.name', 'outlook.com'], + ['user.name+tag', 'live.co.uk', 'user.name', 'outlook.com'], + ]; + + foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { + $result = $this->provider->normalize($inputLocal, $inputDomain); + $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); + $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); + } + } + + public function test_get_canonical_domain(): void + { + $this->assertEquals('outlook.com', $this->provider->getCanonicalDomain()); + } + + public function test_get_supported_domains(): void + { + $domains = $this->provider->getSupportedDomains(); + $expected = ['outlook.com', 'hotmail.com', 'live.com', 'outlook.co.uk', 'hotmail.co.uk', 'live.co.uk']; + $this->assertEquals($expected, $domains); + } +} diff --git a/tests/Normalizer/Providers/ProtonmailTest.php b/tests/Normalizer/Providers/ProtonmailTest.php new file mode 100644 index 0000000..bead1c9 --- /dev/null +++ b/tests/Normalizer/Providers/ProtonmailTest.php @@ -0,0 +1,85 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Normalizer\Providers; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Normalizer\Providers\Protonmail; + +class ProtonmailTest extends TestCase +{ + private Protonmail $provider; + + protected function setUp(): void + { + $this->provider = new Protonmail; + } + + public function test_supports(): void + { + $this->assertTrue($this->provider->supports('protonmail.com')); + $this->assertTrue($this->provider->supports('proton.me')); + $this->assertTrue($this->provider->supports('pm.me')); + $this->assertFalse($this->provider->supports('gmail.com')); + $this->assertFalse($this->provider->supports('outlook.com')); + $this->assertFalse($this->provider->supports('example.com')); + } + + public function test_normalize(): void + { + $testCases = [ + ['user.name+tag', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+spam', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+newsletter', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+work', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+personal', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+test123', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+anything', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+verylongtag', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+tag.with.dots', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+tag-with-hyphens', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+tag_with_underscores', 'protonmail.com', 'username', 'protonmail.com'], + ['user.name+tag123', 'protonmail.com', 'username', 'protonmail.com'], + // Other ProtonMail domains + ['user.name+tag', 'proton.me', 'username', 'protonmail.com'], + ['user.name+tag', 'pm.me', 'username', 'protonmail.com'], + // Dots are preserved for ProtonMail + ['user.name', 'protonmail.com', 'user.name', 'protonmail.com'], + ['u.s.e.r.n.a.m.e', 'protonmail.com', 'u.s.e.r.n.a.m.e', 'protonmail.com'], + // Edge cases + ['user+', 'protonmail.com', 'user', 'protonmail.com'], + ['user.', 'protonmail.com', 'user.', 'protonmail.com'], + ['.user', 'protonmail.com', '.user', 'protonmail.com'], + ]; + + foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { + $result = $this->provider->normalize($inputLocal, $inputDomain); + $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); + $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); + } + } + + public function test_get_canonical_domain(): void + { + $this->assertEquals('protonmail.com', $this->provider->getCanonicalDomain()); + } + + public function test_get_supported_domains(): void + { + $domains = $this->provider->getSupportedDomains(); + $expected = ['protonmail.com', 'proton.me', 'pm.me']; + $this->assertEquals($expected, $domains); + } +} diff --git a/tests/Normalizer/Providers/YahooTest.php b/tests/Normalizer/Providers/YahooTest.php new file mode 100644 index 0000000..5976aad --- /dev/null +++ b/tests/Normalizer/Providers/YahooTest.php @@ -0,0 +1,104 @@ + + * + * @version 1.0 RC4 + * + * @license The MIT License (MIT) + */ + +namespace Utopia\Tests\Normalizer\Providers; + +use PHPUnit\Framework\TestCase; +use Utopia\Emails\Normalizer\Providers\Yahoo; + +class YahooTest extends TestCase +{ + private Yahoo $provider; + + protected function setUp(): void + { + $this->provider = new Yahoo; + } + + public function test_supports(): void + { + $this->assertTrue($this->provider->supports('yahoo.com')); + $this->assertTrue($this->provider->supports('yahoo.co.uk')); + $this->assertTrue($this->provider->supports('yahoo.ca')); + $this->assertTrue($this->provider->supports('ymail.com')); + $this->assertTrue($this->provider->supports('rocketmail.com')); + $this->assertFalse($this->provider->supports('gmail.com')); + $this->assertFalse($this->provider->supports('outlook.com')); + $this->assertFalse($this->provider->supports('example.com')); + } + + public function test_normalize(): void + { + $testCases = [ + ['user.name+tag', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+spam', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+newsletter', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+work', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+personal', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+test123', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+anything', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+verylongtag', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+tag.with.dots', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+tag-with-hyphens', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+tag_with_underscores', 'yahoo.com', 'username', 'yahoo.com'], + ['user.name+tag123', 'yahoo.com', 'username', 'yahoo.com'], + // Hyphen removal + ['user-name', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+tag', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+spam', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+newsletter', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+work', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+personal', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+test123', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+anything', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+verylongtag', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+tag.with.dots', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+tag-with-hyphens', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+tag_with_underscores', 'yahoo.com', 'username', 'yahoo.com'], + ['user-name+tag123', 'yahoo.com', 'username', 'yahoo.com'], + // Multiple hyphens + ['u-s-e-r-n-a-m-e', 'yahoo.com', 'username', 'yahoo.com'], + ['u-s-e-r-n-a-m-e+tag', 'yahoo.com', 'username', 'yahoo.com'], + // Other Yahoo domains + ['user.name+tag', 'yahoo.co.uk', 'username', 'yahoo.com'], + ['user.name+tag', 'yahoo.ca', 'username', 'yahoo.com'], + ['user.name+tag', 'ymail.com', 'username', 'yahoo.com'], + ['user.name+tag', 'rocketmail.com', 'username', 'yahoo.com'], + // Edge cases + ['user+', 'yahoo.com', 'user', 'yahoo.com'], + ['user-', 'yahoo.com', 'user', 'yahoo.com'], + ['user.', 'yahoo.com', 'user.', 'yahoo.com'], + ['.user', 'yahoo.com', '.user', 'yahoo.com'], + ]; + + foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { + $result = $this->provider->normalize($inputLocal, $inputDomain); + $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); + $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); + } + } + + public function test_get_canonical_domain(): void + { + $this->assertEquals('yahoo.com', $this->provider->getCanonicalDomain()); + } + + public function test_get_supported_domains(): void + { + $domains = $this->provider->getSupportedDomains(); + $expected = ['yahoo.com', 'yahoo.co.uk', 'yahoo.ca', 'ymail.com', 'rocketmail.com']; + $this->assertEquals($expected, $domains); + } +} From a7da806b2a7e65cd820e67207eba3f5f62274e26 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 09:19:49 +0100 Subject: [PATCH 10/21] Remove --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 1f657df..8cddb25 100644 --- a/README.md +++ b/README.md @@ -334,10 +334,6 @@ Utopia Emails requires PHP 8.0 or later. We recommend using the latest PHP versi + [https://twitter.com/eldadfux](https://twitter.com/eldadfux) + [https://github.com/eldadfux](https://github.com/eldadfux) -**Wess Cope** - -+ [https://github.com/wesscope](https://github.com/wesscope) - ## Copyright and license The MIT License (MIT) [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php) From c0588ce3d9404458beedb8f844498050498ea780 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 09:22:55 +0100 Subject: [PATCH 11/21] Update GitHub Actions workflow to rename job from CodeQL to Composer Checks and modify steps to use actions/checkout@v4 for improved performance. --- .github/workflows/codeql-analysis.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6436663..e062e9d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,20 +1,16 @@ -name: "CodeQL" +name: "Composer Checks" on: [pull_request] jobs: lint: - name: CodeQL + name: Composer Checks runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 - with: - fetch-depth: 2 + uses: actions/checkout@v4 - - run: git checkout HEAD^2 - - - name: Run CodeQL + - name: Run Composer Checks run: | docker run --rm -v $PWD:/app composer:2.6 sh -c \ "composer install --profile --ignore-platform-reqs && composer check" From a54616f3a9e5af6f8eeb7b8d9ae43dd0c9c764de Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 19:44:48 +0100 Subject: [PATCH 12/21] Enhance Email class by introducing formatting constants and updating README for clarity on usage; refactor getFormatted method to utilize constants instead of strings. --- README.md | 24 ++++++++++++++++++------ src/Emails/Email.php | 21 +++++++++++++++------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8cddb25..ad1b079 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,11 @@ $email->getSubdomain(); // mail $email->hasSubdomain(); // true // Email formatting -$email->getFormatted('full'); // user@mail.example.com -$email->getFormatted('local'); // user -$email->getFormatted('domain'); // mail.example.com -$email->getFormatted('provider'); // example.com -$email->getFormatted('subdomain'); // mail +$email->getFormatted(Email::FORMAT_FULL); // user@mail.example.com +$email->getFormatted(Email::FORMAT_LOCAL); // user +$email->getFormatted(Email::FORMAT_DOMAIN); // mail.example.com +$email->getFormatted(Email::FORMAT_PROVIDER); // example.com +$email->getFormatted(Email::FORMAT_SUBDOMAIN); // mail // Email normalization $email = new Email(' USER@EXAMPLE.COM '); @@ -74,6 +74,18 @@ $email->normalize(); // user@example.com ### Email Class +#### Constants + +The Email class provides the following constants for email formatting: + +- `Email::FORMAT_FULL` - Full email address (default) +- `Email::FORMAT_LOCAL` - Local part only (before @) +- `Email::FORMAT_DOMAIN` - Domain part only (after @) +- `Email::FORMAT_PROVIDER` - Provider domain (domain without subdomain) +- `Email::FORMAT_SUBDOMAIN` - Subdomain part only + +#### Methods + * **get()** - Return full email address. * **getLocal()** - Return local part (before @). * **getDomain()** - Return domain part (after @). @@ -89,7 +101,7 @@ $email->normalize(); // user@example.com * **getSubdomain()** - Get email subdomain (if any). * **hasSubdomain()** - Check if email has subdomain. * **normalize()** - Normalize email address (remove extra spaces, convert to lowercase). -* **getFormatted(string $format)** - Get email in different formats ('full', 'local', 'domain', 'provider', 'subdomain'). +* **getFormatted(string $format)** - Get email in different formats. Use constants: `Email::FORMAT_FULL`, `Email::FORMAT_LOCAL`, `Email::FORMAT_DOMAIN`, `Email::FORMAT_PROVIDER`, `Email::FORMAT_SUBDOMAIN`. ## Using the Validators diff --git a/src/Emails/Email.php b/src/Emails/Email.php index 33aeae3..0dc181d 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -24,6 +24,15 @@ class Email */ public const DOMAIN_MAX_LENGTH = 253; + /** + * Email format constants + */ + public const FORMAT_FULL = 'full'; + public const FORMAT_LOCAL = 'local'; + public const FORMAT_DOMAIN = 'domain'; + public const FORMAT_PROVIDER = 'provider'; + public const FORMAT_SUBDOMAIN = 'subdomain'; + /** * Email address * @@ -373,18 +382,18 @@ protected function isDomainSupported(string $domain): bool /** * Get email in different formats */ - public function getFormatted(string $format = 'full'): string + public function getFormatted(string $format = self::FORMAT_FULL): string { switch ($format) { - case 'local': + case self::FORMAT_LOCAL: return $this->local; - case 'domain': + case self::FORMAT_DOMAIN: return $this->domain; - case 'provider': + case self::FORMAT_PROVIDER: return $this->getProvider(); - case 'subdomain': + case self::FORMAT_SUBDOMAIN: return $this->getSubdomain(); - case 'full': + case self::FORMAT_FULL: default: return $this->email; } From 903f85e311b5e7010d5d14cdacba0bcfc789bb4d Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:00:46 +0100 Subject: [PATCH 13/21] Add additional email format constants to Email class for enhanced flexibility in email handling --- src/Emails/Email.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Emails/Email.php b/src/Emails/Email.php index 0dc181d..e010ed4 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -28,9 +28,13 @@ class Email * Email format constants */ public const FORMAT_FULL = 'full'; + public const FORMAT_LOCAL = 'local'; + public const FORMAT_DOMAIN = 'domain'; + public const FORMAT_PROVIDER = 'provider'; + public const FORMAT_SUBDOMAIN = 'subdomain'; /** From 2e7b66073c9ad1d1dbc91cf886523ffc039713a3 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:13:11 +0100 Subject: [PATCH 14/21] Comment out plus addressing and dots removal in email normalization providers until manual confirmation is obtained for their support across Fastmail, Generic, iCloud, Outlook, ProtonMail, and Yahoo. Update test cases accordingly to reflect these changes. --- src/Emails/Normalizer/Providers/Fastmail.php | 13 +- src/Emails/Normalizer/Providers/Generic.php | 15 +- src/Emails/Normalizer/Providers/Icloud.php | 13 +- src/Emails/Normalizer/Providers/Outlook.php | 5 +- .../Normalizer/Providers/Protonmail.php | 13 +- src/Emails/Normalizer/Providers/Yahoo.php | 18 +- tests/EmailTest.php | 399 +++++++++++------- tests/Normalizer/Providers/FastmailTest.php | 31 +- tests/Normalizer/Providers/GenericTest.php | 40 +- tests/Normalizer/Providers/IcloudTest.php | 36 +- tests/Normalizer/Providers/OutlookTest.php | 47 ++- tests/Normalizer/Providers/ProtonmailTest.php | 36 +- tests/Normalizer/Providers/YahooTest.php | 85 ++-- 13 files changed, 428 insertions(+), 323 deletions(-) diff --git a/src/Emails/Normalizer/Providers/Fastmail.php b/src/Emails/Normalizer/Providers/Fastmail.php index 75082ef..150c2fb 100644 --- a/src/Emails/Normalizer/Providers/Fastmail.php +++ b/src/Emails/Normalizer/Providers/Fastmail.php @@ -8,7 +8,7 @@ * Fastmail * * Handles Fastmail email normalization - * - Removes plus addressing + * - TODO: Plus addressing and dots removal commented out until manual confirmation * - Preserves dots and hyphens in local part * - Normalizes to fastmail.com domain */ @@ -28,16 +28,17 @@ public function normalize(string $local, string $domain): array // Convert to lowercase $normalizedLocal = $this->toLowerCase($local); + // TODO: Commented out until manual confirmation of Fastmail's plus addressing and dots support // Check if there's plus addressing - $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + // $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; // Remove plus addressing (everything after +) - $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + // $normalizedLocal = $this->removePlusAddressing($normalizedLocal); // Remove dots only if there was plus addressing (Fastmail treats dots as aliases only with plus) - if ($hasPlus) { - $normalizedLocal = $this->removeDots($normalizedLocal); - } + // if ($hasPlus) { + // $normalizedLocal = $this->removeDots($normalizedLocal); + // } return [ 'local' => $normalizedLocal, diff --git a/src/Emails/Normalizer/Providers/Generic.php b/src/Emails/Normalizer/Providers/Generic.php index 9f888f8..cbb9cd1 100644 --- a/src/Emails/Normalizer/Providers/Generic.php +++ b/src/Emails/Normalizer/Providers/Generic.php @@ -8,7 +8,7 @@ * Generic * * Handles generic email normalization for unsupported providers - * - Only removes plus addressing + * - TODO: Plus addressing, dots, and hyphens removal commented out until manual confirmation * - Preserves all other characters */ class Generic extends Provider @@ -24,17 +24,18 @@ public function normalize(string $local, string $domain): array // Convert to lowercase $normalizedLocal = $this->toLowerCase($local); + // TODO: Commented out until manual confirmation of generic providers' plus addressing, dots, and hyphens support // Check if there's plus addressing - $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + // $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; // Remove plus addressing (everything after +) - $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + // $normalizedLocal = $this->removePlusAddressing($normalizedLocal); // Remove dots and hyphens only if there was plus addressing (generic providers treat these as aliases only with plus) - if ($hasPlus) { - $normalizedLocal = $this->removeDots($normalizedLocal); - $normalizedLocal = $this->removeHyphens($normalizedLocal); - } + // if ($hasPlus) { + // $normalizedLocal = $this->removeDots($normalizedLocal); + // $normalizedLocal = $this->removeHyphens($normalizedLocal); + // } return [ 'local' => $normalizedLocal, diff --git a/src/Emails/Normalizer/Providers/Icloud.php b/src/Emails/Normalizer/Providers/Icloud.php index 61c316b..543ffe1 100644 --- a/src/Emails/Normalizer/Providers/Icloud.php +++ b/src/Emails/Normalizer/Providers/Icloud.php @@ -8,7 +8,7 @@ * iCloud * * Handles Apple iCloud email normalization - * - Removes plus addressing + * - TODO: Plus addressing and dots removal commented out until manual confirmation * - Preserves dots and hyphens in local part * - Normalizes to icloud.com domain */ @@ -28,16 +28,17 @@ public function normalize(string $local, string $domain): array // Convert to lowercase $normalizedLocal = $this->toLowerCase($local); + // TODO: Commented out until manual confirmation of iCloud's plus addressing and dots support // Check if there's plus addressing - $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + // $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; // Remove plus addressing (everything after +) - $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + // $normalizedLocal = $this->removePlusAddressing($normalizedLocal); // Remove dots only if there was plus addressing (iCloud treats dots as aliases only with plus) - if ($hasPlus) { - $normalizedLocal = $this->removeDots($normalizedLocal); - } + // if ($hasPlus) { + // $normalizedLocal = $this->removeDots($normalizedLocal); + // } return [ 'local' => $normalizedLocal, diff --git a/src/Emails/Normalizer/Providers/Outlook.php b/src/Emails/Normalizer/Providers/Outlook.php index 0eac594..3d5de1d 100644 --- a/src/Emails/Normalizer/Providers/Outlook.php +++ b/src/Emails/Normalizer/Providers/Outlook.php @@ -8,7 +8,7 @@ * Outlook * * Handles Outlook, Hotmail, and Live email normalization - * - Removes plus addressing + * - TODO: Plus addressing removal commented out until manual confirmation * - Preserves dots in local part * - Normalizes to outlook.com domain */ @@ -31,8 +31,9 @@ public function normalize(string $local, string $domain): array // Convert to lowercase $normalizedLocal = $this->toLowerCase($local); + // TODO: Commented out until manual confirmation of Outlook's plus addressing support // Remove plus addressing (everything after +) - $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + // $normalizedLocal = $this->removePlusAddressing($normalizedLocal); return [ 'local' => $normalizedLocal, diff --git a/src/Emails/Normalizer/Providers/Protonmail.php b/src/Emails/Normalizer/Providers/Protonmail.php index dc6ff79..a6ecffc 100644 --- a/src/Emails/Normalizer/Providers/Protonmail.php +++ b/src/Emails/Normalizer/Providers/Protonmail.php @@ -8,7 +8,7 @@ * ProtonMail * * Handles ProtonMail email normalization - * - Removes plus addressing + * - TODO: Plus addressing and dots removal commented out until manual confirmation * - Preserves dots and hyphens in local part * - Normalizes to protonmail.com domain */ @@ -28,16 +28,17 @@ public function normalize(string $local, string $domain): array // Convert to lowercase $normalizedLocal = $this->toLowerCase($local); + // TODO: Commented out until manual confirmation of ProtonMail's plus addressing and dots support // Check if there's plus addressing - $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + // $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; // Remove plus addressing (everything after +) - $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + // $normalizedLocal = $this->removePlusAddressing($normalizedLocal); // Remove dots only if there was plus addressing (ProtonMail treats dots as aliases only with plus) - if ($hasPlus) { - $normalizedLocal = $this->removeDots($normalizedLocal); - } + // if ($hasPlus) { + // $normalizedLocal = $this->removeDots($normalizedLocal); + // } return [ 'local' => $normalizedLocal, diff --git a/src/Emails/Normalizer/Providers/Yahoo.php b/src/Emails/Normalizer/Providers/Yahoo.php index d3308f5..ca187bb 100644 --- a/src/Emails/Normalizer/Providers/Yahoo.php +++ b/src/Emails/Normalizer/Providers/Yahoo.php @@ -8,9 +8,8 @@ * Yahoo * * Handles Yahoo email normalization - * - Removes plus addressing - * - Removes hyphens from local part - * - Preserves dots in local part + * - TODO: Plus addressing, dots, and hyphens removal commented out until manual confirmation + * - Preserves dots and hyphens in local part * - Normalizes to yahoo.com domain */ class Yahoo extends Provider @@ -32,19 +31,20 @@ public function normalize(string $local, string $domain): array // Convert to lowercase $normalizedLocal = $this->toLowerCase($local); + // TODO: Commented out until manual confirmation of Yahoo's plus addressing, dots, and hyphens support // Check if there's plus addressing - $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; + // $hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0; // Remove plus addressing (everything after +) - $normalizedLocal = $this->removePlusAddressing($normalizedLocal); + // $normalizedLocal = $this->removePlusAddressing($normalizedLocal); // Remove dots only if there was plus addressing (Yahoo treats dots as aliases only with plus) - if ($hasPlus) { - $normalizedLocal = $this->removeDots($normalizedLocal); - } + // if ($hasPlus) { + // $normalizedLocal = $this->removeDots($normalizedLocal); + // } // Remove hyphens (Yahoo treats hyphens as aliases) - $normalizedLocal = $this->removeHyphens($normalizedLocal); + // $normalizedLocal = $this->removeHyphens($normalizedLocal); return [ 'local' => $normalizedLocal, diff --git a/tests/EmailTest.php b/tests/EmailTest.php index 9de2366..c31745b 100644 --- a/tests/EmailTest.php +++ b/tests/EmailTest.php @@ -423,38 +423,47 @@ public function test_get_unique_gmail_aliases(): void public function test_get_unique_outlook_aliases(): void { $testCases = [ - // Outlook/Hotmail/Live plus addressing - ['user.name+tag@outlook.com', 'user.name@outlook.com'], - ['user.name+spam@outlook.com', 'user.name@outlook.com'], - ['user.name+newsletter@outlook.com', 'user.name@outlook.com'], - ['user.name+work@outlook.com', 'user.name@outlook.com'], - ['user.name+personal@outlook.com', 'user.name@outlook.com'], - ['user.name+test123@outlook.com', 'user.name@outlook.com'], - ['user.name+anything@outlook.com', 'user.name@outlook.com'], - ['user.name+verylongtag@outlook.com', 'user.name@outlook.com'], - ['user.name+tag.with.dots@outlook.com', 'user.name@outlook.com'], - ['user.name+tag-with-hyphens@outlook.com', 'user.name@outlook.com'], - ['user.name+tag_with_underscores@outlook.com', 'user.name@outlook.com'], - ['user.name+tag123@outlook.com', 'user.name@outlook.com'], - // Hotmail - ['user.name+tag@hotmail.com', 'user.name@outlook.com'], - ['user.name+spam@hotmail.com', 'user.name@outlook.com'], - ['user.name@hotmail.com', 'user.name@outlook.com'], - // Live - ['user.name+tag@live.com', 'user.name@outlook.com'], - ['user.name+spam@live.com', 'user.name@outlook.com'], - ['user.name@live.com', 'user.name@outlook.com'], - // UK variants - ['user.name+tag@outlook.co.uk', 'user.name@outlook.com'], - ['user.name+tag@hotmail.co.uk', 'user.name@outlook.com'], - ['user.name+tag@live.co.uk', 'user.name@outlook.com'], + // TODO: Commented out until manual confirmation of Outlook's plus addressing support + // // Outlook/Hotmail/Live plus addressing + // ['user.name+tag@outlook.com', 'user.name@outlook.com'], + // ['user.name+spam@outlook.com', 'user.name@outlook.com'], + // ['user.name+newsletter@outlook.com', 'user.name@outlook.com'], + // ['user.name+work@outlook.com', 'user.name@outlook.com'], + // ['user.name+personal@outlook.com', 'user.name@outlook.com'], + // ['user.name+test123@outlook.com', 'user.name@outlook.com'], + // ['user.name+anything@outlook.com', 'user.name@outlook.com'], + // ['user.name+verylongtag@outlook.com', 'user.name@outlook.com'], + // ['user.name+tag.with.dots@outlook.com', 'user.name@outlook.com'], + // ['user.name+tag-with-hyphens@outlook.com', 'user.name@outlook.com'], + // ['user.name+tag_with_underscores@outlook.com', 'user.name@outlook.com'], + // ['user.name+tag123@outlook.com', 'user.name@outlook.com'], + // // Hotmail + // ['user.name+tag@hotmail.com', 'user.name@outlook.com'], + // ['user.name+spam@hotmail.com', 'user.name@outlook.com'], + // ['user.name@hotmail.com', 'user.name@outlook.com'], + // // Live + // ['user.name+tag@live.com', 'user.name@outlook.com'], + // ['user.name+spam@live.com', 'user.name@outlook.com'], + // ['user.name@live.com', 'user.name@outlook.com'], + // // UK variants + // ['user.name+tag@outlook.co.uk', 'user.name@outlook.com'], + // ['user.name+tag@hotmail.co.uk', 'user.name@outlook.com'], + // ['user.name+tag@live.co.uk', 'user.name@outlook.com'], // Dots are preserved for Outlook ['user.name@outlook.com', 'user.name@outlook.com'], ['u.s.e.r.n.a.m.e@outlook.com', 'u.s.e.r.n.a.m.e@outlook.com'], // Edge cases - ['user+@outlook.com', 'user@outlook.com'], + // ['user+@outlook.com', 'user@outlook.com'], ['user.@outlook.com', 'user.@outlook.com'], ['.user@outlook.com', '.user@outlook.com'], + // Hotmail + ['user.name@hotmail.com', 'user.name@outlook.com'], + // Live + ['user.name@live.com', 'user.name@outlook.com'], + // UK variants + ['user.name@outlook.co.uk', 'user.name@outlook.com'], + ['user.name@hotmail.co.uk', 'user.name@outlook.com'], + ['user.name@live.co.uk', 'user.name@outlook.com'], ]; foreach ($testCases as [$input, $expected]) { @@ -466,46 +475,57 @@ public function test_get_unique_outlook_aliases(): void public function test_get_unique_yahoo_aliases(): void { $testCases = [ - // Yahoo plus addressing and hyphen removal - ['user.name+tag@yahoo.com', 'username@yahoo.com'], - ['user.name+spam@yahoo.com', 'username@yahoo.com'], - ['user.name+newsletter@yahoo.com', 'username@yahoo.com'], - ['user.name+work@yahoo.com', 'username@yahoo.com'], - ['user.name+personal@yahoo.com', 'username@yahoo.com'], - ['user.name+test123@yahoo.com', 'username@yahoo.com'], - ['user.name+anything@yahoo.com', 'username@yahoo.com'], - ['user.name+verylongtag@yahoo.com', 'username@yahoo.com'], - ['user.name+tag.with.dots@yahoo.com', 'username@yahoo.com'], - ['user.name+tag-with-hyphens@yahoo.com', 'username@yahoo.com'], - ['user.name+tag_with_underscores@yahoo.com', 'username@yahoo.com'], - ['user.name+tag123@yahoo.com', 'username@yahoo.com'], - // Hyphen removal - ['user-name@yahoo.com', 'username@yahoo.com'], - ['user-name+tag@yahoo.com', 'username@yahoo.com'], - ['user-name+spam@yahoo.com', 'username@yahoo.com'], - ['user-name+newsletter@yahoo.com', 'username@yahoo.com'], - ['user-name+work@yahoo.com', 'username@yahoo.com'], - ['user-name+personal@yahoo.com', 'username@yahoo.com'], - ['user-name+test123@yahoo.com', 'username@yahoo.com'], - ['user-name+anything@yahoo.com', 'username@yahoo.com'], - ['user-name+verylongtag@yahoo.com', 'username@yahoo.com'], - ['user-name+tag.with.dots@yahoo.com', 'username@yahoo.com'], - ['user-name+tag-with-hyphens@yahoo.com', 'username@yahoo.com'], - ['user-name+tag_with_underscores@yahoo.com', 'username@yahoo.com'], - ['user-name+tag123@yahoo.com', 'username@yahoo.com'], - // Multiple hyphens - ['u-s-e-r-n-a-m-e@yahoo.com', 'username@yahoo.com'], - ['u-s-e-r-n-a-m-e+tag@yahoo.com', 'username@yahoo.com'], - // Other Yahoo domains - ['user.name+tag@yahoo.co.uk', 'username@yahoo.com'], - ['user.name+tag@yahoo.ca', 'username@yahoo.com'], - ['user.name+tag@ymail.com', 'username@yahoo.com'], - ['user.name+tag@rocketmail.com', 'username@yahoo.com'], - // Edge cases - ['user+@yahoo.com', 'user@yahoo.com'], - ['user-@yahoo.com', 'user@yahoo.com'], + // TODO: Commented out until manual confirmation of Yahoo's plus addressing, dots, and hyphens support + // // Yahoo plus addressing and hyphen removal + // ['user.name+tag@yahoo.com', 'username@yahoo.com'], + // ['user.name+spam@yahoo.com', 'username@yahoo.com'], + // ['user.name+newsletter@yahoo.com', 'username@yahoo.com'], + // ['user.name+work@yahoo.com', 'username@yahoo.com'], + // ['user.name+personal@yahoo.com', 'username@yahoo.com'], + // ['user.name+test123@yahoo.com', 'username@yahoo.com'], + // ['user.name+anything@yahoo.com', 'username@yahoo.com'], + // ['user.name+verylongtag@yahoo.com', 'username@yahoo.com'], + // ['user.name+tag.with.dots@yahoo.com', 'username@yahoo.com'], + // ['user.name+tag-with-hyphens@yahoo.com', 'username@yahoo.com'], + // ['user.name+tag_with_underscores@yahoo.com', 'username@yahoo.com'], + // ['user.name+tag123@yahoo.com', 'username@yahoo.com'], + // // Hyphen removal + // ['user-name@yahoo.com', 'username@yahoo.com'], + // ['user-name+tag@yahoo.com', 'username@yahoo.com'], + // ['user-name+spam@yahoo.com', 'username@yahoo.com'], + // ['user-name+newsletter@yahoo.com', 'username@yahoo.com'], + // ['user-name+work@yahoo.com', 'username@yahoo.com'], + // ['user-name+personal@yahoo.com', 'username@yahoo.com'], + // ['user-name+test123@yahoo.com', 'username@yahoo.com'], + // ['user-name+anything@yahoo.com', 'username@yahoo.com'], + // ['user-name+verylongtag@yahoo.com', 'username@yahoo.com'], + // ['user-name+tag.with.dots@yahoo.com', 'username@yahoo.com'], + // ['user-name+tag-with-hyphens@yahoo.com', 'username@yahoo.com'], + // ['user-name+tag_with_underscores@yahoo.com', 'username@yahoo.com'], + // ['user-name+tag123@yahoo.com', 'username@yahoo.com'], + // // Multiple hyphens + // ['u-s-e-r-n-a-m-e@yahoo.com', 'username@yahoo.com'], + // ['u-s-e-r-n-a-m-e+tag@yahoo.com', 'username@yahoo.com'], + // // Other Yahoo domains + // ['user.name+tag@yahoo.co.uk', 'username@yahoo.com'], + // ['user.name+tag@yahoo.ca', 'username@yahoo.com'], + // ['user.name+tag@ymail.com', 'username@yahoo.com'], + // ['user.name+tag@rocketmail.com', 'username@yahoo.com'], + // // Edge cases + // ['user+@yahoo.com', 'user@yahoo.com'], + // ['user-@yahoo.com', 'user@yahoo.com'], + // Dots and hyphens are preserved for Yahoo + ['user.name@yahoo.com', 'user.name@yahoo.com'], + ['user-name@yahoo.com', 'user-name@yahoo.com'], + ['u.s.e.r.n.a.m.e@yahoo.com', 'u.s.e.r.n.a.m.e@yahoo.com'], + ['u-s-e-r-n-a-m-e@yahoo.com', 'u-s-e-r-n-a-m-e@yahoo.com'], ['user.@yahoo.com', 'user.@yahoo.com'], ['.user@yahoo.com', '.user@yahoo.com'], + // Other Yahoo domains + ['user.name@yahoo.co.uk', 'user.name@yahoo.com'], + ['user.name@yahoo.ca', 'user.name@yahoo.com'], + ['user.name@ymail.com', 'user.name@yahoo.com'], + ['user.name@rocketmail.com', 'user.name@yahoo.com'], ]; foreach ($testCases as [$input, $expected]) { @@ -517,29 +537,33 @@ public function test_get_unique_yahoo_aliases(): void public function test_get_unique_icloud_aliases(): void { $testCases = [ - // iCloud plus addressing - ['user.name+tag@icloud.com', 'username@icloud.com'], - ['user.name+spam@icloud.com', 'username@icloud.com'], - ['user.name+newsletter@icloud.com', 'username@icloud.com'], - ['user.name+work@icloud.com', 'username@icloud.com'], - ['user.name+personal@icloud.com', 'username@icloud.com'], - ['user.name+test123@icloud.com', 'username@icloud.com'], - ['user.name+anything@icloud.com', 'username@icloud.com'], - ['user.name+verylongtag@icloud.com', 'username@icloud.com'], - ['user.name+tag.with.dots@icloud.com', 'username@icloud.com'], - ['user.name+tag-with-hyphens@icloud.com', 'username@icloud.com'], - ['user.name+tag_with_underscores@icloud.com', 'username@icloud.com'], - ['user.name+tag123@icloud.com', 'username@icloud.com'], - // Other Apple domains - ['user.name+tag@me.com', 'username@icloud.com'], - ['user.name+tag@mac.com', 'username@icloud.com'], + // TODO: Commented out until manual confirmation of iCloud's plus addressing and dots support + // // iCloud plus addressing + // ['user.name+tag@icloud.com', 'username@icloud.com'], + // ['user.name+spam@icloud.com', 'username@icloud.com'], + // ['user.name+newsletter@icloud.com', 'username@icloud.com'], + // ['user.name+work@icloud.com', 'username@icloud.com'], + // ['user.name+personal@icloud.com', 'username@icloud.com'], + // ['user.name+test123@icloud.com', 'username@icloud.com'], + // ['user.name+anything@icloud.com', 'username@icloud.com'], + // ['user.name+verylongtag@icloud.com', 'username@icloud.com'], + // ['user.name+tag.with.dots@icloud.com', 'username@icloud.com'], + // ['user.name+tag-with-hyphens@icloud.com', 'username@icloud.com'], + // ['user.name+tag_with_underscores@icloud.com', 'username@icloud.com'], + // ['user.name+tag123@icloud.com', 'username@icloud.com'], + // // Other Apple domains + // ['user.name+tag@me.com', 'username@icloud.com'], + // ['user.name+tag@mac.com', 'username@icloud.com'], // Dots are preserved for iCloud ['user.name@icloud.com', 'user.name@icloud.com'], ['u.s.e.r.n.a.m.e@icloud.com', 'u.s.e.r.n.a.m.e@icloud.com'], // Edge cases - ['user+@icloud.com', 'user@icloud.com'], + // ['user+@icloud.com', 'user@icloud.com'], ['user.@icloud.com', 'user.@icloud.com'], ['.user@icloud.com', '.user@icloud.com'], + // Other Apple domains + ['user.name@me.com', 'user.name@icloud.com'], + ['user.name@mac.com', 'user.name@icloud.com'], ]; foreach ($testCases as [$input, $expected]) { @@ -551,29 +575,33 @@ public function test_get_unique_icloud_aliases(): void public function test_get_unique_protonmail_aliases(): void { $testCases = [ - // ProtonMail plus addressing - ['user.name+tag@protonmail.com', 'username@protonmail.com'], - ['user.name+spam@protonmail.com', 'username@protonmail.com'], - ['user.name+newsletter@protonmail.com', 'username@protonmail.com'], - ['user.name+work@protonmail.com', 'username@protonmail.com'], - ['user.name+personal@protonmail.com', 'username@protonmail.com'], - ['user.name+test123@protonmail.com', 'username@protonmail.com'], - ['user.name+anything@protonmail.com', 'username@protonmail.com'], - ['user.name+verylongtag@protonmail.com', 'username@protonmail.com'], - ['user.name+tag.with.dots@protonmail.com', 'username@protonmail.com'], - ['user.name+tag-with-hyphens@protonmail.com', 'username@protonmail.com'], - ['user.name+tag_with_underscores@protonmail.com', 'username@protonmail.com'], - ['user.name+tag123@protonmail.com', 'username@protonmail.com'], - // Other ProtonMail domains - ['user.name+tag@proton.me', 'username@protonmail.com'], - ['user.name+tag@pm.me', 'username@protonmail.com'], + // TODO: Commented out until manual confirmation of ProtonMail's plus addressing and dots support + // // ProtonMail plus addressing + // ['user.name+tag@protonmail.com', 'username@protonmail.com'], + // ['user.name+spam@protonmail.com', 'username@protonmail.com'], + // ['user.name+newsletter@protonmail.com', 'username@protonmail.com'], + // ['user.name+work@protonmail.com', 'username@protonmail.com'], + // ['user.name+personal@protonmail.com', 'username@protonmail.com'], + // ['user.name+test123@protonmail.com', 'username@protonmail.com'], + // ['user.name+anything@protonmail.com', 'username@protonmail.com'], + // ['user.name+verylongtag@protonmail.com', 'username@protonmail.com'], + // ['user.name+tag.with.dots@protonmail.com', 'username@protonmail.com'], + // ['user.name+tag-with-hyphens@protonmail.com', 'username@protonmail.com'], + // ['user.name+tag_with_underscores@protonmail.com', 'username@protonmail.com'], + // ['user.name+tag123@protonmail.com', 'username@protonmail.com'], + // // Other ProtonMail domains + // ['user.name+tag@proton.me', 'username@protonmail.com'], + // ['user.name+tag@pm.me', 'username@protonmail.com'], // Dots are preserved for ProtonMail ['user.name@protonmail.com', 'user.name@protonmail.com'], ['u.s.e.r.n.a.m.e@protonmail.com', 'u.s.e.r.n.a.m.e@protonmail.com'], // Edge cases - ['user+@protonmail.com', 'user@protonmail.com'], + // ['user+@protonmail.com', 'user@protonmail.com'], ['user.@protonmail.com', 'user.@protonmail.com'], ['.user@protonmail.com', '.user@protonmail.com'], + // Other ProtonMail domains + ['user.name@proton.me', 'user.name@protonmail.com'], + ['user.name@pm.me', 'user.name@protonmail.com'], ]; foreach ($testCases as [$input, $expected]) { @@ -585,28 +613,31 @@ public function test_get_unique_protonmail_aliases(): void public function test_get_unique_fastmail_aliases(): void { $testCases = [ - // Fastmail plus addressing - ['user.name+tag@fastmail.com', 'username@fastmail.com'], - ['user.name+spam@fastmail.com', 'username@fastmail.com'], - ['user.name+newsletter@fastmail.com', 'username@fastmail.com'], - ['user.name+work@fastmail.com', 'username@fastmail.com'], - ['user.name+personal@fastmail.com', 'username@fastmail.com'], - ['user.name+test123@fastmail.com', 'username@fastmail.com'], - ['user.name+anything@fastmail.com', 'username@fastmail.com'], - ['user.name+verylongtag@fastmail.com', 'username@fastmail.com'], - ['user.name+tag.with.dots@fastmail.com', 'username@fastmail.com'], - ['user.name+tag-with-hyphens@fastmail.com', 'username@fastmail.com'], - ['user.name+tag_with_underscores@fastmail.com', 'username@fastmail.com'], - ['user.name+tag123@fastmail.com', 'username@fastmail.com'], - // Other Fastmail domain - ['user.name+tag@fastmail.fm', 'username@fastmail.com'], + // TODO: Commented out until manual confirmation of Fastmail's plus addressing and dots support + // // Fastmail plus addressing + // ['user.name+tag@fastmail.com', 'username@fastmail.com'], + // ['user.name+spam@fastmail.com', 'username@fastmail.com'], + // ['user.name+newsletter@fastmail.com', 'username@fastmail.com'], + // ['user.name+work@fastmail.com', 'username@fastmail.com'], + // ['user.name+personal@fastmail.com', 'username@fastmail.com'], + // ['user.name+test123@fastmail.com', 'username@fastmail.com'], + // ['user.name+anything@fastmail.com', 'username@fastmail.com'], + // ['user.name+verylongtag@fastmail.com', 'username@fastmail.com'], + // ['user.name+tag.with.dots@fastmail.com', 'username@fastmail.com'], + // ['user.name+tag-with-hyphens@fastmail.com', 'username@fastmail.com'], + // ['user.name+tag_with_underscores@fastmail.com', 'username@fastmail.com'], + // ['user.name+tag123@fastmail.com', 'username@fastmail.com'], + // // Other Fastmail domain + // ['user.name+tag@fastmail.fm', 'username@fastmail.com'], // Dots are preserved for Fastmail ['user.name@fastmail.com', 'user.name@fastmail.com'], ['u.s.e.r.n.a.m.e@fastmail.com', 'u.s.e.r.n.a.m.e@fastmail.com'], // Edge cases - ['user+@fastmail.com', 'user@fastmail.com'], + // ['user+@fastmail.com', 'user@fastmail.com'], ['user.@fastmail.com', 'user.@fastmail.com'], ['.user@fastmail.com', '.user@fastmail.com'], + // Other Fastmail domain + ['user.name@fastmail.fm', 'user.name@fastmail.com'], ]; foreach ($testCases as [$input, $expected]) { @@ -618,27 +649,28 @@ public function test_get_unique_fastmail_aliases(): void public function test_get_unique_other_domains(): void { $testCases = [ - // Other domains with plus addressing - ['user.name+tag@example.com', 'username@example.com'], - ['user.name+spam@example.com', 'username@example.com'], - ['user.name+newsletter@example.com', 'username@example.com'], - ['user.name+work@example.com', 'username@example.com'], - ['user.name+personal@example.com', 'username@example.com'], - ['user.name+test123@example.com', 'username@example.com'], - ['user.name+anything@example.com', 'username@example.com'], - ['user.name+verylongtag@example.com', 'username@example.com'], - ['user.name+tag.with.dots@example.com', 'username@example.com'], - ['user.name+tag-with-hyphens@example.com', 'username@example.com'], - ['user.name+tag_with_underscores@example.com', 'username@example.com'], - ['user.name+tag123@example.com', 'username@example.com'], + // TODO: Commented out until manual confirmation of generic providers' plus addressing, dots, and hyphens support + // // Other domains with plus addressing + // ['user.name+tag@example.com', 'username@example.com'], + // ['user.name+spam@example.com', 'username@example.com'], + // ['user.name+newsletter@example.com', 'username@example.com'], + // ['user.name+work@example.com', 'username@example.com'], + // ['user.name+personal@example.com', 'username@example.com'], + // ['user.name+test123@example.com', 'username@example.com'], + // ['user.name+anything@example.com', 'username@example.com'], + // ['user.name+verylongtag@example.com', 'username@example.com'], + // ['user.name+tag.with.dots@example.com', 'username@example.com'], + // ['user.name+tag-with-hyphens@example.com', 'username@example.com'], + // ['user.name+tag_with_underscores@example.com', 'username@example.com'], + // ['user.name+tag123@example.com', 'username@example.com'], // Dots are preserved for other domains ['user.name@example.com', 'user.name@example.com'], ['u.s.e.r.n.a.m.e@example.com', 'u.s.e.r.n.a.m.e@example.com'], // Hyphens are preserved for other domains ['user-name@example.com', 'user-name@example.com'], - ['user-name+tag@example.com', 'username@example.com'], + // ['user-name+tag@example.com', 'username@example.com'], // Edge cases - ['user+@example.com', 'user@example.com'], + // ['user+@example.com', 'user@example.com'], ['user.@example.com', 'user.@example.com'], ['.user@example.com', '.user@example.com'], ]; @@ -654,12 +686,14 @@ public function test_get_unique_edge_cases(): void $testCases = [ // Empty plus addressing ['user+@gmail.com', 'user@gmail.com'], - ['user+@outlook.com', 'user@outlook.com'], - ['user+@yahoo.com', 'user@yahoo.com'], - ['user+@icloud.com', 'user@icloud.com'], - ['user+@protonmail.com', 'user@protonmail.com'], - ['user+@fastmail.com', 'user@fastmail.com'], - ['user+@example.com', 'user@example.com'], + // TODO: Commented out until manual confirmation of Outlook's plus addressing support + // ['user+@outlook.com', 'user@outlook.com'], + // TODO: Commented out until manual confirmation of non-Gmail providers' plus addressing support + // ['user+@yahoo.com', 'user@yahoo.com'], + // ['user+@icloud.com', 'user@icloud.com'], + // ['user+@protonmail.com', 'user@protonmail.com'], + // ['user+@fastmail.com', 'user@fastmail.com'], + // ['user+@example.com', 'user@example.com'], // Plus at the beginning ['+user@gmail.com', '+user@gmail.com'], ['+user@outlook.com', '+user@outlook.com'], @@ -670,12 +704,14 @@ public function test_get_unique_edge_cases(): void ['+user@example.com', '+user@example.com'], // Multiple plus signs (only first one is considered) ['user+tag+more@gmail.com', 'user@gmail.com'], - ['user+tag+more@outlook.com', 'user@outlook.com'], - ['user+tag+more@yahoo.com', 'user@yahoo.com'], - ['user+tag+more@icloud.com', 'user@icloud.com'], - ['user+tag+more@protonmail.com', 'user@protonmail.com'], - ['user+tag+more@fastmail.com', 'user@fastmail.com'], - ['user+tag+more@example.com', 'user@example.com'], + // TODO: Commented out until manual confirmation of Outlook's plus addressing support + // ['user+tag+more@outlook.com', 'user@outlook.com'], + // TODO: Commented out until manual confirmation of non-Gmail providers' plus addressing support + // ['user+tag+more@yahoo.com', 'user@yahoo.com'], + // ['user+tag+more@icloud.com', 'user@icloud.com'], + // ['user+tag+more@protonmail.com', 'user@protonmail.com'], + // ['user+tag+more@fastmail.com', 'user@fastmail.com'], + // ['user+tag+more@example.com', 'user@example.com'], // Special characters in plus addressing ['user+tag!@gmail.com', 'user@gmail.com'], ['user+tag#@gmail.com', 'user@gmail.com'], @@ -718,24 +754,46 @@ public function test_get_unique_case_sensitivity(): void ['USER.NAME+TAG@GMAIL.COM', 'username@gmail.com'], ['User.Name+Tag@Gmail.Com', 'username@gmail.com'], ['user.name+tag@Gmail.com', 'username@gmail.com'], - ['USER.NAME+TAG@OUTLOOK.COM', 'user.name@outlook.com'], - ['User.Name+Tag@Outlook.Com', 'user.name@outlook.com'], - ['user.name+tag@Outlook.com', 'user.name@outlook.com'], - ['USER.NAME+TAG@YAHOO.COM', 'username@yahoo.com'], - ['User.Name+Tag@Yahoo.Com', 'username@yahoo.com'], - ['user.name+tag@Yahoo.com', 'username@yahoo.com'], - ['USER.NAME+TAG@ICLOUD.COM', 'username@icloud.com'], - ['User.Name+Tag@Icloud.Com', 'username@icloud.com'], - ['user.name+tag@Icloud.com', 'username@icloud.com'], - ['USER.NAME+TAG@PROTONMAIL.COM', 'username@protonmail.com'], - ['User.Name+Tag@Protonmail.Com', 'username@protonmail.com'], - ['user.name+tag@Protonmail.com', 'username@protonmail.com'], - ['USER.NAME+TAG@FASTMAIL.COM', 'username@fastmail.com'], - ['User.Name+Tag@Fastmail.Com', 'username@fastmail.com'], - ['user.name+tag@Fastmail.com', 'username@fastmail.com'], - ['USER.NAME+TAG@EXAMPLE.COM', 'username@example.com'], - ['User.Name+Tag@Example.Com', 'username@example.com'], - ['user.name+tag@Example.com', 'username@example.com'], + // TODO: Commented out until manual confirmation of Outlook's plus addressing support + // ['USER.NAME+TAG@OUTLOOK.COM', 'user.name@outlook.com'], + // ['User.Name+Tag@Outlook.Com', 'user.name@outlook.com'], + // ['user.name+tag@Outlook.com', 'user.name@outlook.com'], + // Dots are preserved for Outlook + ['USER.NAME@OUTLOOK.COM', 'user.name@outlook.com'], + ['User.Name@Outlook.Com', 'user.name@outlook.com'], + ['user.name@Outlook.com', 'user.name@outlook.com'], + // TODO: Commented out until manual confirmation of non-Gmail providers' plus addressing and dots support + // ['USER.NAME+TAG@YAHOO.COM', 'username@yahoo.com'], + // ['User.Name+Tag@Yahoo.Com', 'username@yahoo.com'], + // ['user.name+tag@Yahoo.com', 'username@yahoo.com'], + // ['USER.NAME+TAG@ICLOUD.COM', 'username@icloud.com'], + // ['User.Name+Tag@Icloud.Com', 'username@icloud.com'], + // ['user.name+tag@Icloud.com', 'username@icloud.com'], + // ['USER.NAME+TAG@PROTONMAIL.COM', 'username@protonmail.com'], + // ['User.Name+Tag@Protonmail.Com', 'username@protonmail.com'], + // ['user.name+tag@Protonmail.com', 'username@protonmail.com'], + // ['USER.NAME+TAG@FASTMAIL.COM', 'username@fastmail.com'], + // ['User.Name+Tag@Fastmail.Com', 'username@fastmail.com'], + // ['user.name+tag@Fastmail.com', 'username@fastmail.com'], + // ['USER.NAME+TAG@EXAMPLE.COM', 'username@example.com'], + // ['User.Name+Tag@Example.Com', 'username@example.com'], + // ['user.name+tag@Example.com', 'username@example.com'], + // Dots and pluses are preserved for non-Gmail providers + ['USER.NAME@YAHOO.COM', 'user.name@yahoo.com'], + ['User.Name@Yahoo.Com', 'user.name@yahoo.com'], + ['user.name@Yahoo.com', 'user.name@yahoo.com'], + ['USER.NAME@ICLOUD.COM', 'user.name@icloud.com'], + ['User.Name@Icloud.Com', 'user.name@icloud.com'], + ['user.name@Icloud.com', 'user.name@icloud.com'], + ['USER.NAME@PROTONMAIL.COM', 'user.name@protonmail.com'], + ['User.Name@Protonmail.Com', 'user.name@protonmail.com'], + ['user.name@Protonmail.com', 'user.name@protonmail.com'], + ['USER.NAME@FASTMAIL.COM', 'user.name@fastmail.com'], + ['User.Name@Fastmail.Com', 'user.name@fastmail.com'], + ['user.name@Fastmail.com', 'user.name@fastmail.com'], + ['USER.NAME@EXAMPLE.COM', 'user.name@example.com'], + ['User.Name@Example.Com', 'user.name@example.com'], + ['user.name@Example.com', 'user.name@example.com'], ]; foreach ($testCases as [$input, $expected]) { @@ -830,13 +888,26 @@ public function test_get_unique_with_different_providers(): void $gmailEmail = new Email('user.name+tag@gmail.com'); $this->assertEquals('username@gmail.com', $gmailEmail->getUnique()); - $outlookEmail = new Email('user.name+tag@outlook.com'); + // TODO: Commented out until manual confirmation of Outlook's plus addressing support + // $outlookEmail = new Email('user.name+tag@outlook.com'); + // $this->assertEquals('user.name@outlook.com', $outlookEmail->getUnique()); + + // Dots are preserved for Outlook + $outlookEmail = new Email('user.name@outlook.com'); $this->assertEquals('user.name@outlook.com', $outlookEmail->getUnique()); - $yahooEmail = new Email('user-name+tag@yahoo.com'); - $this->assertEquals('username@yahoo.com', $yahooEmail->getUnique()); + // TODO: Commented out until manual confirmation of non-Gmail providers' plus addressing and dots support + // $yahooEmail = new Email('user-name+tag@yahoo.com'); + // $this->assertEquals('username@yahoo.com', $yahooEmail->getUnique()); + + // $genericEmail = new Email('user.name+tag@example.com'); + // $this->assertEquals('username@example.com', $genericEmail->getUnique()); + + // Dots and pluses are preserved for non-Gmail providers + $yahooEmail = new Email('user-name@yahoo.com'); + $this->assertEquals('user-name@yahoo.com', $yahooEmail->getUnique()); - $genericEmail = new Email('user.name+tag@example.com'); - $this->assertEquals('username@example.com', $genericEmail->getUnique()); + $genericEmail = new Email('user.name@example.com'); + $this->assertEquals('user.name@example.com', $genericEmail->getUnique()); } } diff --git a/tests/Normalizer/Providers/FastmailTest.php b/tests/Normalizer/Providers/FastmailTest.php index b97527f..511b76d 100644 --- a/tests/Normalizer/Providers/FastmailTest.php +++ b/tests/Normalizer/Providers/FastmailTest.php @@ -39,25 +39,26 @@ public function test_supports(): void public function test_normalize(): void { $testCases = [ - ['user.name+tag', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+spam', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+newsletter', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+work', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+personal', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+test123', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+anything', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+verylongtag', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+tag.with.dots', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+tag-with-hyphens', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+tag_with_underscores', 'fastmail.com', 'username', 'fastmail.com'], - ['user.name+tag123', 'fastmail.com', 'username', 'fastmail.com'], - // Other Fastmail domain - ['user.name+tag', 'fastmail.fm', 'username', 'fastmail.com'], + // TODO: Commented out until manual confirmation of Fastmail's plus addressing and dots support + // ['user.name+tag', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+spam', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+newsletter', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+work', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+personal', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+test123', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+anything', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+verylongtag', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+tag.with.dots', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+tag-with-hyphens', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+tag_with_underscores', 'fastmail.com', 'username', 'fastmail.com'], + // ['user.name+tag123', 'fastmail.com', 'username', 'fastmail.com'], + // // Other Fastmail domain + // ['user.name+tag', 'fastmail.fm', 'username', 'fastmail.com'], // Dots are preserved for Fastmail ['user.name', 'fastmail.com', 'user.name', 'fastmail.com'], ['u.s.e.r.n.a.m.e', 'fastmail.com', 'u.s.e.r.n.a.m.e', 'fastmail.com'], // Edge cases - ['user+', 'fastmail.com', 'user', 'fastmail.com'], + // ['user+', 'fastmail.com', 'user', 'fastmail.com'], ['user.', 'fastmail.com', 'user.', 'fastmail.com'], ['.user', 'fastmail.com', '.user', 'fastmail.com'], ]; diff --git a/tests/Normalizer/Providers/GenericTest.php b/tests/Normalizer/Providers/GenericTest.php index 4856ff5..3af296c 100644 --- a/tests/Normalizer/Providers/GenericTest.php +++ b/tests/Normalizer/Providers/GenericTest.php @@ -42,33 +42,37 @@ public function test_supports(): void public function test_normalize(): void { $testCases = [ - // Other domains with plus addressing - ['user.name+tag', 'example.com', 'username', 'example.com'], - ['user.name+spam', 'example.com', 'username', 'example.com'], - ['user.name+newsletter', 'example.com', 'username', 'example.com'], - ['user.name+work', 'example.com', 'username', 'example.com'], - ['user.name+personal', 'example.com', 'username', 'example.com'], - ['user.name+test123', 'example.com', 'username', 'example.com'], - ['user.name+anything', 'example.com', 'username', 'example.com'], - ['user.name+verylongtag', 'example.com', 'username', 'example.com'], - ['user.name+tag.with.dots', 'example.com', 'username', 'example.com'], - ['user.name+tag-with-hyphens', 'example.com', 'username', 'example.com'], - ['user.name+tag_with_underscores', 'example.com', 'username', 'example.com'], - ['user.name+tag123', 'example.com', 'username', 'example.com'], + // TODO: Commented out until manual confirmation of generic providers' plus addressing, dots, and hyphens support + // // Other domains with plus addressing + // ['user.name+tag', 'example.com', 'username', 'example.com'], + // ['user.name+spam', 'example.com', 'username', 'example.com'], + // ['user.name+newsletter', 'example.com', 'username', 'example.com'], + // ['user.name+work', 'example.com', 'username', 'example.com'], + // ['user.name+personal', 'example.com', 'username', 'example.com'], + // ['user.name+test123', 'example.com', 'username', 'example.com'], + // ['user.name+anything', 'example.com', 'username', 'example.com'], + // ['user.name+verylongtag', 'example.com', 'username', 'example.com'], + // ['user.name+tag.with.dots', 'example.com', 'username', 'example.com'], + // ['user.name+tag-with-hyphens', 'example.com', 'username', 'example.com'], + // ['user.name+tag_with_underscores', 'example.com', 'username', 'example.com'], + // ['user.name+tag123', 'example.com', 'username', 'example.com'], // Dots are preserved for other domains ['user.name', 'example.com', 'user.name', 'example.com'], ['u.s.e.r.n.a.m.e', 'example.com', 'u.s.e.r.n.a.m.e', 'example.com'], // Hyphens are preserved for other domains ['user-name', 'example.com', 'user-name', 'example.com'], - ['user-name+tag', 'example.com', 'username', 'example.com'], + // ['user-name+tag', 'example.com', 'username', 'example.com'], // Edge cases - ['user+', 'example.com', 'user', 'example.com'], + // ['user+', 'example.com', 'user', 'example.com'], ['user.', 'example.com', 'user.', 'example.com'], ['.user', 'example.com', '.user', 'example.com'], // Test with different domains - ['user.name+tag', 'test.org', 'username', 'test.org'], - ['user.name+tag', 'company.net', 'username', 'company.net'], - ['user.name+tag', 'business.co.uk', 'username', 'business.co.uk'], + // ['user.name+tag', 'test.org', 'username', 'test.org'], + // ['user.name+tag', 'company.net', 'username', 'company.net'], + // ['user.name+tag', 'business.co.uk', 'username', 'business.co.uk'], + ['user.name', 'test.org', 'user.name', 'test.org'], + ['user.name', 'company.net', 'user.name', 'company.net'], + ['user.name', 'business.co.uk', 'user.name', 'business.co.uk'], ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { diff --git a/tests/Normalizer/Providers/IcloudTest.php b/tests/Normalizer/Providers/IcloudTest.php index 029d34b..8594cf0 100644 --- a/tests/Normalizer/Providers/IcloudTest.php +++ b/tests/Normalizer/Providers/IcloudTest.php @@ -40,28 +40,32 @@ public function test_supports(): void public function test_normalize(): void { $testCases = [ - ['user.name+tag', 'icloud.com', 'username', 'icloud.com'], - ['user.name+spam', 'icloud.com', 'username', 'icloud.com'], - ['user.name+newsletter', 'icloud.com', 'username', 'icloud.com'], - ['user.name+work', 'icloud.com', 'username', 'icloud.com'], - ['user.name+personal', 'icloud.com', 'username', 'icloud.com'], - ['user.name+test123', 'icloud.com', 'username', 'icloud.com'], - ['user.name+anything', 'icloud.com', 'username', 'icloud.com'], - ['user.name+verylongtag', 'icloud.com', 'username', 'icloud.com'], - ['user.name+tag.with.dots', 'icloud.com', 'username', 'icloud.com'], - ['user.name+tag-with-hyphens', 'icloud.com', 'username', 'icloud.com'], - ['user.name+tag_with_underscores', 'icloud.com', 'username', 'icloud.com'], - ['user.name+tag123', 'icloud.com', 'username', 'icloud.com'], - // Other Apple domains - ['user.name+tag', 'me.com', 'username', 'icloud.com'], - ['user.name+tag', 'mac.com', 'username', 'icloud.com'], + // TODO: Commented out until manual confirmation of iCloud's plus addressing and dots support + // ['user.name+tag', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+spam', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+newsletter', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+work', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+personal', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+test123', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+anything', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+verylongtag', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+tag.with.dots', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+tag-with-hyphens', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+tag_with_underscores', 'icloud.com', 'username', 'icloud.com'], + // ['user.name+tag123', 'icloud.com', 'username', 'icloud.com'], + // // Other Apple domains + // ['user.name+tag', 'me.com', 'username', 'icloud.com'], + // ['user.name+tag', 'mac.com', 'username', 'icloud.com'], // Dots are preserved for iCloud ['user.name', 'icloud.com', 'user.name', 'icloud.com'], ['u.s.e.r.n.a.m.e', 'icloud.com', 'u.s.e.r.n.a.m.e', 'icloud.com'], // Edge cases - ['user+', 'icloud.com', 'user', 'icloud.com'], + // ['user+', 'icloud.com', 'user', 'icloud.com'], ['user.', 'icloud.com', 'user.', 'icloud.com'], ['.user', 'icloud.com', '.user', 'icloud.com'], + // Other Apple domains + ['user.name', 'me.com', 'user.name', 'icloud.com'], + ['user.name', 'mac.com', 'user.name', 'icloud.com'], ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { diff --git a/tests/Normalizer/Providers/OutlookTest.php b/tests/Normalizer/Providers/OutlookTest.php index 471bd1b..97f1764 100644 --- a/tests/Normalizer/Providers/OutlookTest.php +++ b/tests/Normalizer/Providers/OutlookTest.php @@ -43,35 +43,40 @@ public function test_supports(): void public function test_normalize(): void { $testCases = [ - ['user.name+tag', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+spam', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+newsletter', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+work', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+personal', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+test123', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+anything', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+verylongtag', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+tag.with.dots', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+tag-with-hyphens', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+tag_with_underscores', 'outlook.com', 'user.name', 'outlook.com'], - ['user.name+tag123', 'outlook.com', 'user.name', 'outlook.com'], + // TODO: Commented out until manual confirmation of Outlook's plus addressing support + // ['user.name+tag', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+spam', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+newsletter', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+work', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+personal', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+test123', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+anything', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+verylongtag', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+tag.with.dots', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+tag-with-hyphens', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+tag_with_underscores', 'outlook.com', 'user.name', 'outlook.com'], + // ['user.name+tag123', 'outlook.com', 'user.name', 'outlook.com'], + // ['u.s.e.r.n.a.m.e+tag', 'outlook.com', 'u.s.e.r.n.a.m.e', 'outlook.com'], + // ['user+', 'outlook.com', 'user', 'outlook.com'], + // Dots are preserved for Outlook ['u.s.e.r.n.a.m.e', 'outlook.com', 'u.s.e.r.n.a.m.e', 'outlook.com'], - ['u.s.e.r.n.a.m.e+tag', 'outlook.com', 'u.s.e.r.n.a.m.e', 'outlook.com'], - ['user+', 'outlook.com', 'user', 'outlook.com'], ['user.', 'outlook.com', 'user.', 'outlook.com'], ['.user', 'outlook.com', '.user', 'outlook.com'], // Hotmail - ['user.name+tag', 'hotmail.com', 'user.name', 'outlook.com'], - ['user.name+spam', 'hotmail.com', 'user.name', 'outlook.com'], + // ['user.name+tag', 'hotmail.com', 'user.name', 'outlook.com'], + // ['user.name+spam', 'hotmail.com', 'user.name', 'outlook.com'], ['user.name', 'hotmail.com', 'user.name', 'outlook.com'], // Live - ['user.name+tag', 'live.com', 'user.name', 'outlook.com'], - ['user.name+spam', 'live.com', 'user.name', 'outlook.com'], + // ['user.name+tag', 'live.com', 'user.name', 'outlook.com'], + // ['user.name+spam', 'live.com', 'user.name', 'outlook.com'], ['user.name', 'live.com', 'user.name', 'outlook.com'], // UK variants - ['user.name+tag', 'outlook.co.uk', 'user.name', 'outlook.com'], - ['user.name+tag', 'hotmail.co.uk', 'user.name', 'outlook.com'], - ['user.name+tag', 'live.co.uk', 'user.name', 'outlook.com'], + // ['user.name+tag', 'outlook.co.uk', 'user.name', 'outlook.com'], + // ['user.name+tag', 'hotmail.co.uk', 'user.name', 'outlook.com'], + // ['user.name+tag', 'live.co.uk', 'user.name', 'outlook.com'], + ['user.name', 'outlook.co.uk', 'user.name', 'outlook.com'], + ['user.name', 'hotmail.co.uk', 'user.name', 'outlook.com'], + ['user.name', 'live.co.uk', 'user.name', 'outlook.com'], ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { diff --git a/tests/Normalizer/Providers/ProtonmailTest.php b/tests/Normalizer/Providers/ProtonmailTest.php index bead1c9..c03aa0a 100644 --- a/tests/Normalizer/Providers/ProtonmailTest.php +++ b/tests/Normalizer/Providers/ProtonmailTest.php @@ -40,28 +40,32 @@ public function test_supports(): void public function test_normalize(): void { $testCases = [ - ['user.name+tag', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+spam', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+newsletter', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+work', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+personal', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+test123', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+anything', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+verylongtag', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+tag.with.dots', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+tag-with-hyphens', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+tag_with_underscores', 'protonmail.com', 'username', 'protonmail.com'], - ['user.name+tag123', 'protonmail.com', 'username', 'protonmail.com'], - // Other ProtonMail domains - ['user.name+tag', 'proton.me', 'username', 'protonmail.com'], - ['user.name+tag', 'pm.me', 'username', 'protonmail.com'], + // TODO: Commented out until manual confirmation of ProtonMail's plus addressing and dots support + // ['user.name+tag', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+spam', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+newsletter', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+work', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+personal', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+test123', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+anything', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+verylongtag', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+tag.with.dots', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+tag-with-hyphens', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+tag_with_underscores', 'protonmail.com', 'username', 'protonmail.com'], + // ['user.name+tag123', 'protonmail.com', 'username', 'protonmail.com'], + // // Other ProtonMail domains + // ['user.name+tag', 'proton.me', 'username', 'protonmail.com'], + // ['user.name+tag', 'pm.me', 'username', 'protonmail.com'], // Dots are preserved for ProtonMail ['user.name', 'protonmail.com', 'user.name', 'protonmail.com'], ['u.s.e.r.n.a.m.e', 'protonmail.com', 'u.s.e.r.n.a.m.e', 'protonmail.com'], // Edge cases - ['user+', 'protonmail.com', 'user', 'protonmail.com'], + // ['user+', 'protonmail.com', 'user', 'protonmail.com'], ['user.', 'protonmail.com', 'user.', 'protonmail.com'], ['.user', 'protonmail.com', '.user', 'protonmail.com'], + // Other ProtonMail domains + ['user.name', 'proton.me', 'user.name', 'protonmail.com'], + ['user.name', 'pm.me', 'user.name', 'protonmail.com'], ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { diff --git a/tests/Normalizer/Providers/YahooTest.php b/tests/Normalizer/Providers/YahooTest.php index 5976aad..00d3f12 100644 --- a/tests/Normalizer/Providers/YahooTest.php +++ b/tests/Normalizer/Providers/YahooTest.php @@ -42,45 +42,56 @@ public function test_supports(): void public function test_normalize(): void { $testCases = [ - ['user.name+tag', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+spam', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+newsletter', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+work', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+personal', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+test123', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+anything', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+verylongtag', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+tag.with.dots', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+tag-with-hyphens', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+tag_with_underscores', 'yahoo.com', 'username', 'yahoo.com'], - ['user.name+tag123', 'yahoo.com', 'username', 'yahoo.com'], - // Hyphen removal - ['user-name', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+tag', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+spam', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+newsletter', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+work', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+personal', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+test123', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+anything', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+verylongtag', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+tag.with.dots', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+tag-with-hyphens', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+tag_with_underscores', 'yahoo.com', 'username', 'yahoo.com'], - ['user-name+tag123', 'yahoo.com', 'username', 'yahoo.com'], - // Multiple hyphens - ['u-s-e-r-n-a-m-e', 'yahoo.com', 'username', 'yahoo.com'], - ['u-s-e-r-n-a-m-e+tag', 'yahoo.com', 'username', 'yahoo.com'], - // Other Yahoo domains - ['user.name+tag', 'yahoo.co.uk', 'username', 'yahoo.com'], - ['user.name+tag', 'yahoo.ca', 'username', 'yahoo.com'], - ['user.name+tag', 'ymail.com', 'username', 'yahoo.com'], - ['user.name+tag', 'rocketmail.com', 'username', 'yahoo.com'], - // Edge cases - ['user+', 'yahoo.com', 'user', 'yahoo.com'], - ['user-', 'yahoo.com', 'user', 'yahoo.com'], + // TODO: Commented out until manual confirmation of Yahoo's plus addressing, dots, and hyphens support + // ['user.name+tag', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+spam', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+newsletter', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+work', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+personal', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+test123', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+anything', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+verylongtag', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+tag.with.dots', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+tag-with-hyphens', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+tag_with_underscores', 'yahoo.com', 'username', 'yahoo.com'], + // ['user.name+tag123', 'yahoo.com', 'username', 'yahoo.com'], + // // Hyphen removal + // ['user-name', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+tag', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+spam', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+newsletter', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+work', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+personal', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+test123', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+anything', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+verylongtag', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+tag.with.dots', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+tag-with-hyphens', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+tag_with_underscores', 'yahoo.com', 'username', 'yahoo.com'], + // ['user-name+tag123', 'yahoo.com', 'username', 'yahoo.com'], + // // Multiple hyphens + // ['u-s-e-r-n-a-m-e', 'yahoo.com', 'username', 'yahoo.com'], + // ['u-s-e-r-n-a-m-e+tag', 'yahoo.com', 'username', 'yahoo.com'], + // // Other Yahoo domains + // ['user.name+tag', 'yahoo.co.uk', 'username', 'yahoo.com'], + // ['user.name+tag', 'yahoo.ca', 'username', 'yahoo.com'], + // ['user.name+tag', 'ymail.com', 'username', 'yahoo.com'], + // ['user.name+tag', 'rocketmail.com', 'username', 'yahoo.com'], + // // Edge cases + // ['user+', 'yahoo.com', 'user', 'yahoo.com'], + // ['user-', 'yahoo.com', 'user', 'yahoo.com'], + // Dots and hyphens are preserved for Yahoo + ['user.name', 'yahoo.com', 'user.name', 'yahoo.com'], + ['user-name', 'yahoo.com', 'user-name', 'yahoo.com'], + ['u.s.e.r.n.a.m.e', 'yahoo.com', 'u.s.e.r.n.a.m.e', 'yahoo.com'], + ['u-s-e-r-n-a-m-e', 'yahoo.com', 'u-s-e-r-n-a-m-e', 'yahoo.com'], ['user.', 'yahoo.com', 'user.', 'yahoo.com'], ['.user', 'yahoo.com', '.user', 'yahoo.com'], + // Other Yahoo domains + ['user.name', 'yahoo.co.uk', 'user.name', 'yahoo.com'], + ['user.name', 'yahoo.ca', 'user.name', 'yahoo.com'], + ['user.name', 'ymail.com', 'user.name', 'yahoo.com'], + ['user.name', 'rocketmail.com', 'user.name', 'yahoo.com'], ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { From 8ab64f53a270d582a4c7a87d3e36417c677f5d6e Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:38:05 +0100 Subject: [PATCH 15/21] Refactor Email class to implement canonical email normalization, replacing deprecated methods and introducing a new Canonicals namespace. Update README and tests to reflect changes in method names and functionality. --- README.md | 5 +- .../{Normalizer => Canonicals}/Provider.php | 8 ++-- .../Providers/Fastmail.php | 6 +-- .../Providers/Generic.php | 6 +-- .../Providers/Gmail.php | 6 +-- .../Providers/Icloud.php | 6 +-- .../Providers/Outlook.php | 6 +-- .../Providers/Protonmail.php | 6 +-- .../Providers/Yahoo.php | 6 +-- src/Emails/Email.php | 48 +++++++------------ .../Providers/FastmailTest.php | 8 ++-- .../Providers/GenericTest.php | 8 ++-- .../Providers/GmailTest.php | 8 ++-- .../Providers/IcloudTest.php | 8 ++-- .../Providers/OutlookTest.php | 8 ++-- .../Providers/ProtonmailTest.php | 8 ++-- .../Providers/YahooTest.php | 8 ++-- tests/EmailTest.php | 44 ++++++++--------- tests/Validator/EmailCorporateTest.php | 4 +- tests/Validator/EmailDomainTest.php | 4 +- tests/Validator/EmailLocalTest.php | 4 +- tests/Validator/EmailNotDisposableTest.php | 4 +- tests/Validator/EmailTest.php | 8 ++-- 23 files changed, 105 insertions(+), 122 deletions(-) rename src/Emails/{Normalizer => Canonicals}/Provider.php (87%) rename src/Emails/{Normalizer => Canonicals}/Providers/Fastmail.php (90%) rename src/Emails/{Normalizer => Canonicals}/Providers/Generic.php (91%) rename src/Emails/{Normalizer => Canonicals}/Providers/Gmail.php (87%) rename src/Emails/{Normalizer => Canonicals}/Providers/Icloud.php (90%) rename src/Emails/{Normalizer => Canonicals}/Providers/Outlook.php (88%) rename src/Emails/{Normalizer => Canonicals}/Providers/Protonmail.php (90%) rename src/Emails/{Normalizer => Canonicals}/Providers/Yahoo.php (91%) rename tests/{Normalizer => Canonicals}/Providers/FastmailTest.php (93%) rename tests/{Normalizer => Canonicals}/Providers/GenericTest.php (94%) rename tests/{Normalizer => Canonicals}/Providers/GmailTest.php (93%) rename tests/{Normalizer => Canonicals}/Providers/IcloudTest.php (94%) rename tests/{Normalizer => Canonicals}/Providers/OutlookTest.php (95%) rename tests/{Normalizer => Canonicals}/Providers/ProtonmailTest.php (94%) rename tests/{Normalizer => Canonicals}/Providers/YahooTest.php (96%) diff --git a/README.md b/README.md index ad1b079..439eb20 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ $email = new Email('user@example.com'); $email->get(); // user@example.com $email->getLocal(); // user $email->getDomain(); // example.com -$email->getLocalOnly(); // user -$email->getDomainOnly(); // example.com +$email->getLocal(); // user +$email->getDomain(); // example.com $email->isValid(); // true $email->hasValidLocal(); // true $email->hasValidDomain(); // true @@ -89,7 +89,6 @@ The Email class provides the following constants for email formatting: * **get()** - Return full email address. * **getLocal()** - Return local part (before @). * **getDomain()** - Return domain part (after @). -* **getLocalOnly()** - Return email without domain part (local only). * **getDomainOnly()** - Return email without local part (domain only). * **isValid()** - Check if email is valid format. * **hasValidLocal()** - Check if email has valid local part. diff --git a/src/Emails/Normalizer/Provider.php b/src/Emails/Canonicals/Provider.php similarity index 87% rename from src/Emails/Normalizer/Provider.php rename to src/Emails/Canonicals/Provider.php index 0a4db15..e220ac6 100644 --- a/src/Emails/Normalizer/Provider.php +++ b/src/Emails/Canonicals/Provider.php @@ -1,6 +1,6 @@ toLowerCase($local); diff --git a/src/Emails/Normalizer/Providers/Generic.php b/src/Emails/Canonicals/Providers/Generic.php similarity index 91% rename from src/Emails/Normalizer/Providers/Generic.php rename to src/Emails/Canonicals/Providers/Generic.php index cbb9cd1..58cbb0a 100644 --- a/src/Emails/Normalizer/Providers/Generic.php +++ b/src/Emails/Canonicals/Providers/Generic.php @@ -1,8 +1,8 @@ toLowerCase($local); diff --git a/src/Emails/Normalizer/Providers/Gmail.php b/src/Emails/Canonicals/Providers/Gmail.php similarity index 87% rename from src/Emails/Normalizer/Providers/Gmail.php rename to src/Emails/Canonicals/Providers/Gmail.php index ad31e69..bbd6d36 100644 --- a/src/Emails/Normalizer/Providers/Gmail.php +++ b/src/Emails/Canonicals/Providers/Gmail.php @@ -1,8 +1,8 @@ toLowerCase($local); diff --git a/src/Emails/Normalizer/Providers/Icloud.php b/src/Emails/Canonicals/Providers/Icloud.php similarity index 90% rename from src/Emails/Normalizer/Providers/Icloud.php rename to src/Emails/Canonicals/Providers/Icloud.php index 543ffe1..d2d9e1e 100644 --- a/src/Emails/Normalizer/Providers/Icloud.php +++ b/src/Emails/Canonicals/Providers/Icloud.php @@ -1,8 +1,8 @@ toLowerCase($local); diff --git a/src/Emails/Normalizer/Providers/Outlook.php b/src/Emails/Canonicals/Providers/Outlook.php similarity index 88% rename from src/Emails/Normalizer/Providers/Outlook.php rename to src/Emails/Canonicals/Providers/Outlook.php index 3d5de1d..fe57903 100644 --- a/src/Emails/Normalizer/Providers/Outlook.php +++ b/src/Emails/Canonicals/Providers/Outlook.php @@ -1,8 +1,8 @@ toLowerCase($local); diff --git a/src/Emails/Normalizer/Providers/Protonmail.php b/src/Emails/Canonicals/Providers/Protonmail.php similarity index 90% rename from src/Emails/Normalizer/Providers/Protonmail.php rename to src/Emails/Canonicals/Providers/Protonmail.php index a6ecffc..27bdaa6 100644 --- a/src/Emails/Normalizer/Providers/Protonmail.php +++ b/src/Emails/Canonicals/Providers/Protonmail.php @@ -1,8 +1,8 @@ toLowerCase($local); diff --git a/src/Emails/Normalizer/Providers/Yahoo.php b/src/Emails/Canonicals/Providers/Yahoo.php similarity index 91% rename from src/Emails/Normalizer/Providers/Yahoo.php rename to src/Emails/Canonicals/Providers/Yahoo.php index ca187bb..3075f92 100644 --- a/src/Emails/Normalizer/Providers/Yahoo.php +++ b/src/Emails/Canonicals/Providers/Yahoo.php @@ -1,8 +1,8 @@ toLowerCase($local); diff --git a/src/Emails/Email.php b/src/Emails/Email.php index e010ed4..3ec3d77 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -3,14 +3,14 @@ namespace Utopia\Emails; use Exception; -use Utopia\Emails\Normalizer\Provider; -use Utopia\Emails\Normalizer\Providers\Fastmail; -use Utopia\Emails\Normalizer\Providers\Generic; -use Utopia\Emails\Normalizer\Providers\Gmail; -use Utopia\Emails\Normalizer\Providers\Icloud; -use Utopia\Emails\Normalizer\Providers\Outlook; -use Utopia\Emails\Normalizer\Providers\Protonmail; -use Utopia\Emails\Normalizer\Providers\Yahoo; +use Utopia\Emails\Canonicals\Provider; +use Utopia\Emails\Canonicals\Providers\Fastmail; +use Utopia\Emails\Canonicals\Providers\Generic; +use Utopia\Emails\Canonicals\Providers\Gmail; +use Utopia\Emails\Canonicals\Providers\Icloud; +use Utopia\Emails\Canonicals\Providers\Outlook; +use Utopia\Emails\Canonicals\Providers\Protonmail; +use Utopia\Emails\Canonicals\Providers\Yahoo; class Email { @@ -135,22 +135,6 @@ public function getDomain(): string return $this->domain; } - /** - * Return email without local part (domain only) - */ - public function getDomainOnly(): string - { - return $this->domain; - } - - /** - * Return email without domain part (local only) - */ - public function getLocalOnly(): string - { - return $this->local; - } - /** * Check if email is valid format */ @@ -289,30 +273,30 @@ public function hasSubdomain(): bool } /** - * Normalize email address (remove extra spaces, convert to lowercase) + * Get the email address (as provided, just lowercased and trimmed) */ - public function normalize(): string + public function getAddress(): string { return $this->email; } /** - * Get unique email address by removing aliases and provider-specific variations + * Get the canonical email address by removing aliases and provider-specific variations * This method removes plus addressing, dot notation (for Gmail), and other aliasing techniques * to return the canonical form of the email address */ - public function getUnique(): string + public function getCanonical(): string { $provider = $this->getProviderForDomain($this->domain); - $normalized = $provider->normalize($this->local, $this->domain); + $canonical = $provider->getCanonical($this->local, $this->domain); - return $normalized['local'].'@'.$normalized['domain']; + return $canonical['local'].'@'.$canonical['domain']; } /** - * Check if the email domain is supported for normalization + * Check if the email domain is supported for canonical form generation */ - public function isNormalizationSupported(): bool + public function isCanonicalSupported(): bool { return $this->isDomainSupported($this->domain); } diff --git a/tests/Normalizer/Providers/FastmailTest.php b/tests/Canonicals/Providers/FastmailTest.php similarity index 93% rename from tests/Normalizer/Providers/FastmailTest.php rename to tests/Canonicals/Providers/FastmailTest.php index 511b76d..0992bc6 100644 --- a/tests/Normalizer/Providers/FastmailTest.php +++ b/tests/Canonicals/Providers/FastmailTest.php @@ -13,10 +13,10 @@ * @license The MIT License (MIT) */ -namespace Utopia\Tests\Normalizer\Providers; +namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; -use Utopia\Emails\Normalizer\Providers\Fastmail; +use Utopia\Emails\Canonicals\Providers\Fastmail; class FastmailTest extends TestCase { @@ -36,7 +36,7 @@ public function test_supports(): void $this->assertFalse($this->provider->supports('example.com')); } - public function test_normalize(): void + public function test_get_canonical(): void { $testCases = [ // TODO: Commented out until manual confirmation of Fastmail's plus addressing and dots support @@ -64,7 +64,7 @@ public function test_normalize(): void ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { - $result = $this->provider->normalize($inputLocal, $inputDomain); + $result = $this->provider->getCanonical($inputLocal, $inputDomain); $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); } diff --git a/tests/Normalizer/Providers/GenericTest.php b/tests/Canonicals/Providers/GenericTest.php similarity index 94% rename from tests/Normalizer/Providers/GenericTest.php rename to tests/Canonicals/Providers/GenericTest.php index 3af296c..2ebc223 100644 --- a/tests/Normalizer/Providers/GenericTest.php +++ b/tests/Canonicals/Providers/GenericTest.php @@ -13,10 +13,10 @@ * @license The MIT License (MIT) */ -namespace Utopia\Tests\Normalizer\Providers; +namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; -use Utopia\Emails\Normalizer\Providers\Generic; +use Utopia\Emails\Canonicals\Providers\Generic; class GenericTest extends TestCase { @@ -39,7 +39,7 @@ public function test_supports(): void $this->assertTrue($this->provider->supports('any-domain.com')); } - public function test_normalize(): void + public function test_get_canonical(): void { $testCases = [ // TODO: Commented out until manual confirmation of generic providers' plus addressing, dots, and hyphens support @@ -76,7 +76,7 @@ public function test_normalize(): void ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { - $result = $this->provider->normalize($inputLocal, $inputDomain); + $result = $this->provider->getCanonical($inputLocal, $inputDomain); $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); } diff --git a/tests/Normalizer/Providers/GmailTest.php b/tests/Canonicals/Providers/GmailTest.php similarity index 93% rename from tests/Normalizer/Providers/GmailTest.php rename to tests/Canonicals/Providers/GmailTest.php index a64e988..cac55b8 100644 --- a/tests/Normalizer/Providers/GmailTest.php +++ b/tests/Canonicals/Providers/GmailTest.php @@ -13,10 +13,10 @@ * @license The MIT License (MIT) */ -namespace Utopia\Tests\Normalizer\Providers; +namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; -use Utopia\Emails\Normalizer\Providers\Gmail; +use Utopia\Emails\Canonicals\Providers\Gmail; class GmailTest extends TestCase { @@ -36,7 +36,7 @@ public function test_supports(): void $this->assertFalse($this->provider->supports('example.com')); } - public function test_normalize(): void + public function test_get_canonical(): void { $testCases = [ ['user.name', 'gmail.com', 'username', 'gmail.com'], @@ -65,7 +65,7 @@ public function test_normalize(): void ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { - $result = $this->provider->normalize($inputLocal, $inputDomain); + $result = $this->provider->getCanonical($inputLocal, $inputDomain); $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); } diff --git a/tests/Normalizer/Providers/IcloudTest.php b/tests/Canonicals/Providers/IcloudTest.php similarity index 94% rename from tests/Normalizer/Providers/IcloudTest.php rename to tests/Canonicals/Providers/IcloudTest.php index 8594cf0..0b118a5 100644 --- a/tests/Normalizer/Providers/IcloudTest.php +++ b/tests/Canonicals/Providers/IcloudTest.php @@ -13,10 +13,10 @@ * @license The MIT License (MIT) */ -namespace Utopia\Tests\Normalizer\Providers; +namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; -use Utopia\Emails\Normalizer\Providers\Icloud; +use Utopia\Emails\Canonicals\Providers\Icloud; class IcloudTest extends TestCase { @@ -37,7 +37,7 @@ public function test_supports(): void $this->assertFalse($this->provider->supports('example.com')); } - public function test_normalize(): void + public function test_get_canonical(): void { $testCases = [ // TODO: Commented out until manual confirmation of iCloud's plus addressing and dots support @@ -69,7 +69,7 @@ public function test_normalize(): void ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { - $result = $this->provider->normalize($inputLocal, $inputDomain); + $result = $this->provider->getCanonical($inputLocal, $inputDomain); $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); } diff --git a/tests/Normalizer/Providers/OutlookTest.php b/tests/Canonicals/Providers/OutlookTest.php similarity index 95% rename from tests/Normalizer/Providers/OutlookTest.php rename to tests/Canonicals/Providers/OutlookTest.php index 97f1764..d39dfdb 100644 --- a/tests/Normalizer/Providers/OutlookTest.php +++ b/tests/Canonicals/Providers/OutlookTest.php @@ -13,10 +13,10 @@ * @license The MIT License (MIT) */ -namespace Utopia\Tests\Normalizer\Providers; +namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; -use Utopia\Emails\Normalizer\Providers\Outlook; +use Utopia\Emails\Canonicals\Providers\Outlook; class OutlookTest extends TestCase { @@ -40,7 +40,7 @@ public function test_supports(): void $this->assertFalse($this->provider->supports('example.com')); } - public function test_normalize(): void + public function test_get_canonical(): void { $testCases = [ // TODO: Commented out until manual confirmation of Outlook's plus addressing support @@ -80,7 +80,7 @@ public function test_normalize(): void ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { - $result = $this->provider->normalize($inputLocal, $inputDomain); + $result = $this->provider->getCanonical($inputLocal, $inputDomain); $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); } diff --git a/tests/Normalizer/Providers/ProtonmailTest.php b/tests/Canonicals/Providers/ProtonmailTest.php similarity index 94% rename from tests/Normalizer/Providers/ProtonmailTest.php rename to tests/Canonicals/Providers/ProtonmailTest.php index c03aa0a..b6ece3b 100644 --- a/tests/Normalizer/Providers/ProtonmailTest.php +++ b/tests/Canonicals/Providers/ProtonmailTest.php @@ -13,10 +13,10 @@ * @license The MIT License (MIT) */ -namespace Utopia\Tests\Normalizer\Providers; +namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; -use Utopia\Emails\Normalizer\Providers\Protonmail; +use Utopia\Emails\Canonicals\Providers\Protonmail; class ProtonmailTest extends TestCase { @@ -37,7 +37,7 @@ public function test_supports(): void $this->assertFalse($this->provider->supports('example.com')); } - public function test_normalize(): void + public function test_get_canonical(): void { $testCases = [ // TODO: Commented out until manual confirmation of ProtonMail's plus addressing and dots support @@ -69,7 +69,7 @@ public function test_normalize(): void ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { - $result = $this->provider->normalize($inputLocal, $inputDomain); + $result = $this->provider->getCanonical($inputLocal, $inputDomain); $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); } diff --git a/tests/Normalizer/Providers/YahooTest.php b/tests/Canonicals/Providers/YahooTest.php similarity index 96% rename from tests/Normalizer/Providers/YahooTest.php rename to tests/Canonicals/Providers/YahooTest.php index 00d3f12..42eb9b8 100644 --- a/tests/Normalizer/Providers/YahooTest.php +++ b/tests/Canonicals/Providers/YahooTest.php @@ -13,10 +13,10 @@ * @license The MIT License (MIT) */ -namespace Utopia\Tests\Normalizer\Providers; +namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; -use Utopia\Emails\Normalizer\Providers\Yahoo; +use Utopia\Emails\Canonicals\Providers\Yahoo; class YahooTest extends TestCase { @@ -39,7 +39,7 @@ public function test_supports(): void $this->assertFalse($this->provider->supports('example.com')); } - public function test_normalize(): void + public function test_get_canonical(): void { $testCases = [ // TODO: Commented out until manual confirmation of Yahoo's plus addressing, dots, and hyphens support @@ -95,7 +95,7 @@ public function test_normalize(): void ]; foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) { - $result = $this->provider->normalize($inputLocal, $inputDomain); + $result = $this->provider->getCanonical($inputLocal, $inputDomain); $this->assertEquals($expectedLocal, $result['local'], "Failed for local: {$inputLocal}@{$inputDomain}"); $this->assertEquals($expectedDomain, $result['domain'], "Failed for domain: {$inputLocal}@{$inputDomain}"); } diff --git a/tests/EmailTest.php b/tests/EmailTest.php index c31745b..bb2c804 100644 --- a/tests/EmailTest.php +++ b/tests/EmailTest.php @@ -28,8 +28,8 @@ public function test_valid_email(): void $this->assertEquals('test@company.org', $email->get()); $this->assertEquals('test', $email->getLocal()); $this->assertEquals('company.org', $email->getDomain()); - $this->assertEquals('company.org', $email->getDomainOnly()); - $this->assertEquals('test', $email->getLocalOnly()); + $this->assertEquals('company.org', $email->getDomain()); + $this->assertEquals('test', $email->getLocal()); $this->assertEquals(true, $email->isValid()); $this->assertEquals(true, $email->hasValidLocal()); $this->assertEquals(true, $email->hasValidDomain()); @@ -39,7 +39,7 @@ public function test_valid_email(): void $this->assertEquals('company.org', $email->getProvider()); $this->assertEquals('', $email->getSubdomain()); $this->assertEquals(false, $email->hasSubdomain()); - $this->assertEquals('test@company.org', $email->normalize()); + $this->assertEquals('test@company.org', $email->getAddress()); } public function test_email_with_subdomain(): void @@ -167,7 +167,7 @@ public function test_email_normalization(): void $email = new Email(' USER@COMPANY.ORG '); $this->assertEquals('user@company.org', $email->get()); - $this->assertEquals('user@company.org', $email->normalize()); + $this->assertEquals('user@company.org', $email->getAddress()); } public function test_invalid_email_empty(): void @@ -416,7 +416,7 @@ public function test_get_unique_gmail_aliases(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -468,7 +468,7 @@ public function test_get_unique_outlook_aliases(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -530,7 +530,7 @@ public function test_get_unique_yahoo_aliases(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -568,7 +568,7 @@ public function test_get_unique_icloud_aliases(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -606,7 +606,7 @@ public function test_get_unique_protonmail_aliases(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -642,7 +642,7 @@ public function test_get_unique_fastmail_aliases(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -677,7 +677,7 @@ public function test_get_unique_other_domains(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -743,7 +743,7 @@ public function test_get_unique_edge_cases(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -798,7 +798,7 @@ public function test_get_unique_case_sensitivity(): void foreach ($testCases as [$input, $expected]) { $email = new Email($input); - $this->assertEquals($expected, $email->getUnique(), "Failed for input: {$input}"); + $this->assertEquals($expected, $email->getCanonical(), "Failed for input: {$input}"); } } @@ -830,7 +830,7 @@ public function test_is_normalization_supported(): void foreach ($supportedEmails as $emailAddress) { $email = new Email($emailAddress); - $this->assertTrue($email->isNormalizationSupported(), "Email {$emailAddress} should support normalization"); + $this->assertTrue($email->isCanonicalSupported(), "Email {$emailAddress} should support normalization"); } $unsupportedEmails = [ @@ -842,7 +842,7 @@ public function test_is_normalization_supported(): void foreach ($unsupportedEmails as $emailAddress) { $email = new Email($emailAddress); - $this->assertFalse($email->isNormalizationSupported(), "Email {$emailAddress} should not support normalization"); + $this->assertFalse($email->isCanonicalSupported(), "Email {$emailAddress} should not support normalization"); } } @@ -886,28 +886,28 @@ public function test_get_unique_with_different_providers(): void { // Test that different providers are used correctly $gmailEmail = new Email('user.name+tag@gmail.com'); - $this->assertEquals('username@gmail.com', $gmailEmail->getUnique()); + $this->assertEquals('username@gmail.com', $gmailEmail->getCanonical()); // TODO: Commented out until manual confirmation of Outlook's plus addressing support // $outlookEmail = new Email('user.name+tag@outlook.com'); - // $this->assertEquals('user.name@outlook.com', $outlookEmail->getUnique()); + // $this->assertEquals('user.name@outlook.com', $outlookEmail->getCanonical()); // Dots are preserved for Outlook $outlookEmail = new Email('user.name@outlook.com'); - $this->assertEquals('user.name@outlook.com', $outlookEmail->getUnique()); + $this->assertEquals('user.name@outlook.com', $outlookEmail->getCanonical()); // TODO: Commented out until manual confirmation of non-Gmail providers' plus addressing and dots support // $yahooEmail = new Email('user-name+tag@yahoo.com'); - // $this->assertEquals('username@yahoo.com', $yahooEmail->getUnique()); + // $this->assertEquals('username@yahoo.com', $yahooEmail->getCanonical()); // $genericEmail = new Email('user.name+tag@example.com'); - // $this->assertEquals('username@example.com', $genericEmail->getUnique()); + // $this->assertEquals('username@example.com', $genericEmail->getCanonical()); // Dots and pluses are preserved for non-Gmail providers $yahooEmail = new Email('user-name@yahoo.com'); - $this->assertEquals('user-name@yahoo.com', $yahooEmail->getUnique()); + $this->assertEquals('user-name@yahoo.com', $yahooEmail->getCanonical()); $genericEmail = new Email('user.name@example.com'); - $this->assertEquals('user.name@example.com', $genericEmail->getUnique()); + $this->assertEquals('user.name@example.com', $genericEmail->getCanonical()); } } diff --git a/tests/Validator/EmailCorporateTest.php b/tests/Validator/EmailCorporateTest.php index 5af4288..7f850a1 100644 --- a/tests/Validator/EmailCorporateTest.php +++ b/tests/Validator/EmailCorporateTest.php @@ -100,14 +100,14 @@ public function test_non_string_input(): void $this->assertEquals(false, $validator->isValid(false)); } - public function test_validator_description(): void + public function test_validatordescription(): void { $validator = new EmailCorporate; $this->assertEquals('Value must be a valid email address from a corporate domain', $validator->getDescription()); } - public function test_validator_type(): void + public function test_validatortype(): void { $validator = new EmailCorporate; diff --git a/tests/Validator/EmailDomainTest.php b/tests/Validator/EmailDomainTest.php index ff7fee1..ebe0420 100644 --- a/tests/Validator/EmailDomainTest.php +++ b/tests/Validator/EmailDomainTest.php @@ -60,14 +60,14 @@ public function test_non_string_input(): void $this->assertEquals(false, $validator->isValid(false)); } - public function test_validator_description(): void + public function test_validatordescription(): void { $validator = new EmailDomain; $this->assertEquals('Value must be a valid email address with a valid domain', $validator->getDescription()); } - public function test_validator_type(): void + public function test_validatortype(): void { $validator = new EmailDomain; diff --git a/tests/Validator/EmailLocalTest.php b/tests/Validator/EmailLocalTest.php index 3b22224..205d132 100644 --- a/tests/Validator/EmailLocalTest.php +++ b/tests/Validator/EmailLocalTest.php @@ -56,14 +56,14 @@ public function test_non_string_input(): void $this->assertEquals(false, $validator->isValid(false)); } - public function test_validator_description(): void + public function test_validatordescription(): void { $validator = new EmailLocal; $this->assertEquals('Value must be a valid email address with a valid local part', $validator->getDescription()); } - public function test_validator_type(): void + public function test_validatortype(): void { $validator = new EmailLocal; diff --git a/tests/Validator/EmailNotDisposableTest.php b/tests/Validator/EmailNotDisposableTest.php index 600e9dd..7a8ea29 100644 --- a/tests/Validator/EmailNotDisposableTest.php +++ b/tests/Validator/EmailNotDisposableTest.php @@ -75,14 +75,14 @@ public function test_non_string_input(): void $this->assertEquals(false, $validator->isValid(false)); } - public function test_validator_description(): void + public function test_validatordescription(): void { $validator = new EmailNotDisposable; $this->assertEquals('Value must be a valid email address that is not from a disposable email service', $validator->getDescription()); } - public function test_validator_type(): void + public function test_validatortype(): void { $validator = new EmailNotDisposable; diff --git a/tests/Validator/EmailTest.php b/tests/Validator/EmailTest.php index ff4121e..471c440 100644 --- a/tests/Validator/EmailTest.php +++ b/tests/Validator/EmailTest.php @@ -20,7 +20,7 @@ class EmailTest extends TestCase { - public function test_valid_email(): void + public function test_validemail(): void { $validator = new Email; @@ -34,7 +34,7 @@ public function test_valid_email(): void $this->assertEquals(true, $validator->isValid('user@mail.sub.example.com')); } - public function test_invalid_email(): void + public function test_invalidemail(): void { $validator = new Email; @@ -71,14 +71,14 @@ public function test_non_string_input(): void $this->assertEquals(false, $validator->isValid(false)); } - public function test_validator_description(): void + public function test_validatordescription(): void { $validator = new Email; $this->assertEquals('Value must be a valid email address', $validator->getDescription()); } - public function test_validator_type(): void + public function test_validatortype(): void { $validator = new Email; From c9cb016bf5e2d1182c352e971b9ff60dbfe4a699 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:40:22 +0100 Subject: [PATCH 16/21] Enhance Email class by introducing a Domain instance for improved domain parsing and validation. Refactor getProvider, getSubdomain, and hasSubdomain methods to utilize the new Domain functionality, ensuring better handling of email domains. --- src/Emails/Email.php | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Emails/Email.php b/src/Emails/Email.php index 3ec3d77..7499dfd 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -3,6 +3,7 @@ namespace Utopia\Emails; use Exception; +use Utopia\Domains\Domain; use Utopia\Emails\Canonicals\Provider; use Utopia\Emails\Canonicals\Providers\Fastmail; use Utopia\Emails\Canonicals\Providers\Generic; @@ -86,6 +87,13 @@ class Email */ protected static $providers = null; + /** + * Domain instance for domain parsing + * + * @var Domain + */ + protected $domainInstance = null; + /** * Email constructor. */ @@ -109,6 +117,9 @@ public function __construct(string $email) if (empty($this->local) || empty($this->domain)) { throw new Exception("'{$email}' must be a valid email address"); } + + // Initialize domain instance for domain parsing + $this->domainInstance = new Domain($this->domain); } /** @@ -186,6 +197,11 @@ public function hasValidDomain(): bool return false; } + // Use utopia domains to check if domain is known and valid + if (!$this->domainInstance->isKnown() && !$this->domainInstance->isTest()) { + return false; + } + return true; } @@ -236,18 +252,15 @@ public function isCorporate(): bool */ public function getProvider(): string { - $domainParts = explode('.', $this->domain); - - if (count($domainParts) < 2) { + // Use utopia domains to get the registerable domain (provider) + $registerable = $this->domainInstance->getRegisterable(); + + // If registerable domain is not available, fall back to the full domain + if (empty($registerable)) { return $this->domain; } - // For domains like mail.company.com, return company.com - if (count($domainParts) > 2) { - return implode('.', array_slice($domainParts, -2)); - } - - return $this->domain; + return $registerable; } /** @@ -255,13 +268,8 @@ public function getProvider(): string */ public function getSubdomain(): string { - $domainParts = explode('.', $this->domain); - - if (count($domainParts) <= 2) { - return ''; - } - - return implode('.', array_slice($domainParts, 0, -2)); + // Use utopia domains to get the subdomain + return $this->domainInstance->getSub(); } /** @@ -269,7 +277,7 @@ public function getSubdomain(): string */ public function hasSubdomain(): bool { - return ! empty($this->getSubdomain()); + return ! empty($this->domainInstance->getSub()); } /** From 8fa5a6bd14c4feee0ef47b657bde35b10e155716 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:44:00 +0100 Subject: [PATCH 17/21] Refactor Email class and clean up test files by removing unnecessary comments and whitespace. Update domain validation logic for improved clarity and consistency. --- src/Emails/Email.php | 4 ++-- tests/Canonicals/Providers/FastmailTest.php | 13 ------------- tests/Canonicals/Providers/GenericTest.php | 13 ------------- tests/Canonicals/Providers/GmailTest.php | 13 ------------- tests/Canonicals/Providers/IcloudTest.php | 13 ------------- tests/Canonicals/Providers/OutlookTest.php | 13 ------------- tests/Canonicals/Providers/ProtonmailTest.php | 13 ------------- tests/Canonicals/Providers/YahooTest.php | 13 ------------- tests/EmailTest.php | 13 ------------- tests/Validator/EmailCorporateTest.php | 13 ------------- tests/Validator/EmailDomainTest.php | 13 ------------- tests/Validator/EmailLocalTest.php | 13 ------------- tests/Validator/EmailNotDisposableTest.php | 13 ------------- tests/Validator/EmailTest.php | 13 ------------- 14 files changed, 2 insertions(+), 171 deletions(-) diff --git a/src/Emails/Email.php b/src/Emails/Email.php index 7499dfd..b48abea 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -198,7 +198,7 @@ public function hasValidDomain(): bool } // Use utopia domains to check if domain is known and valid - if (!$this->domainInstance->isKnown() && !$this->domainInstance->isTest()) { + if (! $this->domainInstance->isKnown() && ! $this->domainInstance->isTest()) { return false; } @@ -254,7 +254,7 @@ public function getProvider(): string { // Use utopia domains to get the registerable domain (provider) $registerable = $this->domainInstance->getRegisterable(); - + // If registerable domain is not available, fall back to the full domain if (empty($registerable)) { return $this->domain; diff --git a/tests/Canonicals/Providers/FastmailTest.php b/tests/Canonicals/Providers/FastmailTest.php index 0992bc6..5f98348 100644 --- a/tests/Canonicals/Providers/FastmailTest.php +++ b/tests/Canonicals/Providers/FastmailTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; diff --git a/tests/Canonicals/Providers/GenericTest.php b/tests/Canonicals/Providers/GenericTest.php index 2ebc223..2c82ea8 100644 --- a/tests/Canonicals/Providers/GenericTest.php +++ b/tests/Canonicals/Providers/GenericTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; diff --git a/tests/Canonicals/Providers/GmailTest.php b/tests/Canonicals/Providers/GmailTest.php index cac55b8..104a21c 100644 --- a/tests/Canonicals/Providers/GmailTest.php +++ b/tests/Canonicals/Providers/GmailTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; diff --git a/tests/Canonicals/Providers/IcloudTest.php b/tests/Canonicals/Providers/IcloudTest.php index 0b118a5..1133d5d 100644 --- a/tests/Canonicals/Providers/IcloudTest.php +++ b/tests/Canonicals/Providers/IcloudTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; diff --git a/tests/Canonicals/Providers/OutlookTest.php b/tests/Canonicals/Providers/OutlookTest.php index d39dfdb..2c2e00a 100644 --- a/tests/Canonicals/Providers/OutlookTest.php +++ b/tests/Canonicals/Providers/OutlookTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; diff --git a/tests/Canonicals/Providers/ProtonmailTest.php b/tests/Canonicals/Providers/ProtonmailTest.php index b6ece3b..8fe0af3 100644 --- a/tests/Canonicals/Providers/ProtonmailTest.php +++ b/tests/Canonicals/Providers/ProtonmailTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; diff --git a/tests/Canonicals/Providers/YahooTest.php b/tests/Canonicals/Providers/YahooTest.php index 42eb9b8..ee1ac5b 100644 --- a/tests/Canonicals/Providers/YahooTest.php +++ b/tests/Canonicals/Providers/YahooTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Canonicals\Providers; use PHPUnit\Framework\TestCase; diff --git a/tests/EmailTest.php b/tests/EmailTest.php index bb2c804..c58e8bb 100644 --- a/tests/EmailTest.php +++ b/tests/EmailTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests; use Exception; diff --git a/tests/Validator/EmailCorporateTest.php b/tests/Validator/EmailCorporateTest.php index 7f850a1..aca44f4 100644 --- a/tests/Validator/EmailCorporateTest.php +++ b/tests/Validator/EmailCorporateTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Validator; use PHPUnit\Framework\TestCase; diff --git a/tests/Validator/EmailDomainTest.php b/tests/Validator/EmailDomainTest.php index ebe0420..8d6c970 100644 --- a/tests/Validator/EmailDomainTest.php +++ b/tests/Validator/EmailDomainTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Validator; use PHPUnit\Framework\TestCase; diff --git a/tests/Validator/EmailLocalTest.php b/tests/Validator/EmailLocalTest.php index 205d132..7654f71 100644 --- a/tests/Validator/EmailLocalTest.php +++ b/tests/Validator/EmailLocalTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Validator; use PHPUnit\Framework\TestCase; diff --git a/tests/Validator/EmailNotDisposableTest.php b/tests/Validator/EmailNotDisposableTest.php index 7a8ea29..19d2649 100644 --- a/tests/Validator/EmailNotDisposableTest.php +++ b/tests/Validator/EmailNotDisposableTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Validator; use PHPUnit\Framework\TestCase; diff --git a/tests/Validator/EmailTest.php b/tests/Validator/EmailTest.php index 471c440..f88dfea 100644 --- a/tests/Validator/EmailTest.php +++ b/tests/Validator/EmailTest.php @@ -1,18 +1,5 @@ - * - * @version 1.0 RC4 - * - * @license The MIT License (MIT) - */ - namespace Utopia\Tests\Validator; use PHPUnit\Framework\TestCase; From 72a1e4ea0144e9eca39b61beb570f9537c150549 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:51:10 +0100 Subject: [PATCH 18/21] Update GitHub Actions workflows to use Composer 2.8 for improved dependency management. Refactor Email class to enhance domain validation and initialization of providers, ensuring better error handling and clarity in logic. --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/linter.yml | 2 +- src/Emails/Email.php | 33 +++++++++++++++------------ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e062e9d..d1dca90 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,6 +12,6 @@ jobs: - name: Run Composer Checks run: | - docker run --rm -v $PWD:/app composer:2.6 sh -c \ + docker run --rm -v $PWD:/app composer:2.8 sh -c \ "composer install --profile --ignore-platform-reqs && composer check" diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 1139d1c..805aad8 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -12,6 +12,6 @@ jobs: - name: Run Linter run: | - docker run --rm -v $PWD:/app composer sh -c \ + docker run --rm -v $PWD:/app composer:2.8 sh -c \ "composer install --profile --ignore-platform-reqs && composer lint" diff --git a/src/Emails/Email.php b/src/Emails/Email.php index b48abea..69dacf2 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -223,7 +223,11 @@ public function isDisposable(): bool public function isFree(): bool { if (self::$freeDomains === null) { - self::$freeDomains = include __DIR__.'/../../data/free-domains.php'; + $data = include __DIR__.'/../../data/free-domains.php'; + if (!is_array($data)) { + throw new Exception('Free domains data file must return an array'); + } + self::$freeDomains = $data; } // If domain is both free and disposable, prioritize disposable classification @@ -239,9 +243,9 @@ public function isFree(): bool */ public function isCorporate(): bool { - // If domain is both free and disposable, prioritize free classification + // If domain is both free and disposable, prioritize disposable classification if ($this->isFree() && $this->isDisposable()) { - return false; // It's free, not corporate + return false; // It's disposable, not corporate } return ! $this->isFree() && ! $this->isDisposable(); @@ -325,9 +329,9 @@ public function getCanonicalDomain(): ?string } /** - * Get the appropriate provider for a given domain + * Initialize providers array */ - protected function getProviderForDomain(string $domain): Provider + protected static function initializeProviders(): void { if (self::$providers === null) { self::$providers = [ @@ -339,6 +343,14 @@ protected function getProviderForDomain(string $domain): Provider new Fastmail, ]; } + } + + /** + * Get the appropriate provider for a given domain + */ + protected function getProviderForDomain(string $domain): Provider + { + self::initializeProviders(); foreach (self::$providers as $provider) { if ($provider->supports($domain)) { @@ -355,16 +367,7 @@ protected function getProviderForDomain(string $domain): Provider */ protected function isDomainSupported(string $domain): bool { - if (self::$providers === null) { - self::$providers = [ - new Gmail, - new Outlook, - new Yahoo, - new Icloud, - new Protonmail, - new Fastmail, - ]; - } + self::initializeProviders(); foreach (self::$providers as $provider) { if ($provider->supports($domain)) { From 46d190ccd59f118e766f1789e8826b64af126659 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:53:02 +0100 Subject: [PATCH 19/21] Enhance Email class by adding validation for the disposable domains data file, ensuring it returns an array before assignment. This improves error handling and robustness in the isDisposable method. --- src/Emails/Email.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Emails/Email.php b/src/Emails/Email.php index 69dacf2..feb82cc 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -211,7 +211,11 @@ public function hasValidDomain(): bool public function isDisposable(): bool { if (self::$disposableDomains === null) { - self::$disposableDomains = include __DIR__.'/../../data/disposable-domains.php'; + $data = include __DIR__.'/../../data/disposable-domains.php'; + if (!is_array($data)) { + throw new Exception('Disposable domains data file must return an array'); + } + self::$disposableDomains = $data; } return in_array($this->domain, self::$disposableDomains); From 9abf9858e8dec2893400764093dc310e221276d5 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:53:51 +0100 Subject: [PATCH 20/21] Refactor Email class to improve code readability by adding whitespace around the 'is_array' checks for disposable and free domains validation. This change enhances consistency in the code style. --- src/Emails/Email.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Emails/Email.php b/src/Emails/Email.php index feb82cc..9b57af3 100644 --- a/src/Emails/Email.php +++ b/src/Emails/Email.php @@ -212,7 +212,7 @@ public function isDisposable(): bool { if (self::$disposableDomains === null) { $data = include __DIR__.'/../../data/disposable-domains.php'; - if (!is_array($data)) { + if (! is_array($data)) { throw new Exception('Disposable domains data file must return an array'); } self::$disposableDomains = $data; @@ -228,7 +228,7 @@ public function isFree(): bool { if (self::$freeDomains === null) { $data = include __DIR__.'/../../data/free-domains.php'; - if (!is_array($data)) { + if (! is_array($data)) { throw new Exception('Free domains data file must return an array'); } self::$freeDomains = $data; From 4d32e60530aba2853ebaa268a3cc6d56fe9224d9 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sun, 19 Oct 2025 20:59:33 +0100 Subject: [PATCH 21/21] Update composer.json to modify PHPStan check command and upgrade PHPStan dependency to version 1.10 for improved static analysis capabilities. --- composer.json | 4 ++-- phpstan.neon | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index c27cdbf..64073db 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "test": "vendor/bin/phpunit", "lint": "./vendor/bin/pint --test", "format": "./vendor/bin/pint", - "check": "./vendor/bin/phpstan analyse --level 4 src tests", + "check": "./vendor/bin/phpstan analyse", "import": "php import.php", "import:all": "php import.php all --commit=true", "import:disposable": "php import.php disposable --commit=true", @@ -34,7 +34,7 @@ "require-dev": { "phpunit/phpunit": "^9.3", "laravel/pint": "1.25.*", - "phpstan/phpstan": "1.9.x-dev" + "phpstan/phpstan": "^1.10" }, "minimum-stability": "stable", "config": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..3d47d11 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 4 + paths: + - src + - tests