Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"require": {
"php": ">=8.0",
"utopia-php/framework": "0.33.*",
"utopia-php/validators": "^0.0.1",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the stability of the utopia-php/validators dependency.

The version constraint ^0.0.1 indicates an early-stage package (0.x.x semantics), which typically implies active development and potential breaking changes. Confirm that this package version is stable enough for production use and that the replacement of utopia-php/framework is intentional and comprehensive.

Please verify the following:

  1. The utopia-php/validators package exists and is appropriate for the enhanced email normalization rules mentioned in the PR.
  2. The version constraint ^0.0.1 is intentional for this library, especially given the 0.x.x versioning.
  3. The utopia-php/framework package is no longer needed elsewhere in the codebase.

Run the following script to check if utopia-php/framework is referenced in other code files:


🏁 Script executed:

#!/bin/bash
# Description: Search for references to utopia-php/framework in the codebase

# Check for direct references in PHP code
rg -n "utopia.*framework" --type=php

# Check for other composer.json files or lock file references
find . -name "composer.lock" -o -name "composer.json" | xargs grep -l "utopia-php/framework" 2>/dev/null || echo "No additional references found"

Length of output: 30


🌐 Web query:

Does the utopia-php/validators package exist on Packagist and what is the purpose of version 0.0.1?

💡 Result:

Short answer: No — there is no utopia-php/validators package listed on Packagist, and therefore there is no published version 0.0.1 on Packagist. Packagist's utopia-php package index (which lists the available utopia-php packages) does not include a "validators" package. [1]

If you have a reference (composer.json, repo, or error) that mentions utopia-php/validators v0.0.1, paste it and I can check whether it's a private package, a typo, or a placeholder/dev artifact. [1]


Non-existent dependency: "utopia-php/validators" does not exist on Packagist.

The utopia-php organization publishes packages like utopia-php/emails (for parsing and validating email addresses), but there is no "validators" package. The composer.json references a package that cannot be installed, causing composer to fail. Verify the correct package name—given the PR context (email normalization), this may be intended as utopia-php/emails instead.

🤖 Prompt for AI Agents
In composer.json around line 29, the dependency "utopia-php/validators":
"^0.0.1" references a non-existent Packagist package and will break composer
install; replace or remove it: determine the intended package (if this PR deals
with email validation, replace with "utopia-php/emails" and pick the correct
version constraint), or if it was meant to be a private/internal package add the
appropriate "repositories" entry and correct package name/version; after
updating composer.json run composer validate and composer update to ensure
dependency resolution succeeds and adjust any code imports/namespaces to match
the chosen package.

"utopia-php/cli": "^0.15",
"utopia-php/domains": "^0.8",
"utopia-php/fetch": "^0.4"
Expand Down
12 changes: 12 additions & 0 deletions src/Emails/Canonicals/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ protected function removePlusAddressing(string $local): string

/**
* Remove all dots from local part
* Can be overridden by providers for custom behavior
*/
protected function removeDots(string $local): string
{
Expand All @@ -62,6 +63,17 @@ protected function removeHyphens(string $local): string
return str_replace('-', '', $local);
}

/**
* Remove hyphen-based subaddress (Yahoo style)
* Removes everything after the last hyphen
*/
protected function removeHyphenSubaddress(string $local): string
{
$components = explode('-', $local);

return count($components) > 1 ? implode('-', array_slice($components, 0, -1)) : $components[0];
}

/**
* Convert local part to lowercase
*/
Expand Down
16 changes: 3 additions & 13 deletions src/Emails/Canonicals/Providers/Fastmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
* Fastmail
*
* Handles Fastmail email normalization
* - TODO: Plus addressing and dots removal commented out until manual confirmation
* - Preserves dots and hyphens in local part
* - Preserves all characters in local part (no subaddress or dot removal)
* - Normalizes to fastmail.com domain
*/
class Fastmail extends Provider
Expand All @@ -28,17 +27,8 @@ public function getCanonical(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;

// 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);
// }
// Fastmail doesn't remove subaddresses or dots
// Just normalize case and domain

return [
'local' => $normalizedLocal,
Expand Down
18 changes: 4 additions & 14 deletions src/Emails/Canonicals/Providers/Generic.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
* Generic
*
* Handles generic email normalization for unsupported providers
* - TODO: Plus addressing, dots, and hyphens removal commented out until manual confirmation
* - Preserves all other characters
* - Preserves all characters in local part (no subaddress or dot removal)
* - Only converts to lowercase
*/
class Generic extends Provider
{
Expand All @@ -24,18 +24,8 @@ public function getCanonical(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;

// 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);
// }
// Generic providers don't remove subaddresses or dots
// Just normalize case

return [
'local' => $normalizedLocal,
Expand Down
16 changes: 11 additions & 5 deletions src/Emails/Canonicals/Providers/Gmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
/**
* Gmail
*
* Handles Gmail and Googlemail email normalization
* Handles Gmail and Googlemail email normalization based on validator.js rules
* - Removes all dots from local part
* - Removes plus addressing
* - Removes plus addressing (subaddress)
* - Normalizes to gmail.com domain
* - Converts googlemail.com to gmail.com
*/
class Gmail extends Provider
{
Expand All @@ -28,11 +29,16 @@ public function getCanonical(string $local, string $domain): array
// Convert to lowercase
$normalizedLocal = $this->toLowerCase($local);

// Remove all dots from local part
// Remove plus addressing (subaddress) - everything after +
$normalizedLocal = $this->removePlusAddressing($normalizedLocal);

// Remove dots from local part
$normalizedLocal = $this->removeDots($normalizedLocal);

// Remove plus addressing (everything after +)
$normalizedLocal = $this->removePlusAddressing($normalizedLocal);
// Ensure local part is not empty after normalization
if (empty($normalizedLocal)) {
throw new \InvalidArgumentException('Email local part cannot be empty after normalization');
}

return [
'local' => $normalizedLocal,
Expand Down
22 changes: 9 additions & 13 deletions src/Emails/Canonicals/Providers/Icloud.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
/**
* iCloud
*
* Handles Apple iCloud email normalization
* - TODO: Plus addressing and dots removal commented out until manual confirmation
* - Preserves dots and hyphens in local part
* Handles Apple iCloud email normalization based on validator.js rules
* - Removes plus addressing (subaddress)
* - Preserves dots in local part
* - Normalizes to icloud.com domain
*/
class Icloud extends Provider
Expand All @@ -28,17 +28,13 @@ public function getCanonical(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;
// Remove plus addressing (subaddress) - everything after +
$normalizedLocal = $this->removePlusAddressing($normalizedLocal);

// 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);
// }
// Ensure local part is not empty after normalization
if (empty($normalizedLocal)) {
throw new \InvalidArgumentException('Email local part cannot be empty after normalization');
}

return [
'local' => $normalizedLocal,
Expand Down
28 changes: 21 additions & 7 deletions src/Emails/Canonicals/Providers/Outlook.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,26 @@
/**
* Outlook
*
* Handles Outlook, Hotmail, and Live email normalization
* - TODO: Plus addressing removal commented out until manual confirmation
* Handles Outlook, Hotmail, and Live email normalization based on validator.js rules
* - Removes plus addressing (subaddress)
* - Preserves dots in local part
* - Normalizes to outlook.com domain
*/
class Outlook extends Provider
{
private const SUPPORTED_DOMAINS = [
'outlook.com', 'hotmail.com', 'live.com',
'outlook.co.uk', 'hotmail.co.uk', 'live.co.uk',
'outlook.com', 'outlook.at', 'outlook.be', 'outlook.cl', 'outlook.co.il', 'outlook.co.nz', 'outlook.co.th', 'outlook.co.uk',
'outlook.com.ar', '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.id', 'outlook.ie',
'outlook.in', 'outlook.it', 'outlook.jp', 'outlook.kr', 'outlook.lv', 'outlook.my', 'outlook.ph', 'outlook.pt',
'outlook.sa', 'outlook.sg', 'outlook.sk',
'hotmail.com', 'hotmail.at', 'hotmail.be', 'hotmail.ca', 'hotmail.cl', 'hotmail.co.il', 'hotmail.co.nz', 'hotmail.co.th', 'hotmail.co.uk',
'hotmail.com.ar', 'hotmail.com.au', 'hotmail.com.br', 'hotmail.com.gr', 'hotmail.com.mx', 'hotmail.com.pe', 'hotmail.com.tr', 'hotmail.com.vn',
'hotmail.cz', 'hotmail.de', 'hotmail.dk', 'hotmail.es', 'hotmail.fr', 'hotmail.hu', 'hotmail.id', 'hotmail.ie',
'hotmail.in', 'hotmail.it', 'hotmail.jp', 'hotmail.kr', 'hotmail.lv', 'hotmail.my', 'hotmail.ph', 'hotmail.pt',
'hotmail.sa', 'hotmail.sg', 'hotmail.sk',
'live.com', 'live.be', 'live.co.uk', 'live.com.ar', 'live.com.mx', 'live.de', 'live.es', 'live.eu', 'live.fr', 'live.it', 'live.nl',
'msn.com', 'passport.com',
];
Comment on lines 17 to 30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the expanded domain list is complete and correct.

The SUPPORTED_DOMAINS list expanded significantly from ~6 to 60+ domains. While this appears comprehensive for Outlook/Hotmail/Live variants, it should be validated against official Microsoft documentation.


🌐 Web query:

What are all the official Outlook, Hotmail, Live, MSN, and Passport email domains supported by Microsoft?

💡 Result:

Short answer — Microsoft supports addresses on these official Microsoft-owned domains (including many country-code variants of each):

  • @outlook.com (and country variants like outlook.co.uk, outlook.de, outlook.fr, etc.). [1]
  • @hotmail.com (and country variants like hotmail.co.uk, hotmail.de, hotmail.fr, hotmail.it, hotmail.co.jp, hotmail.com.au, etc.). [2][3]
  • @live.com (and country variants like live.co.uk, live.de, live.fr, live.it, live.cn, etc.). [2][3]
  • @msn.com (and regional/msn.* variants). [1][2]
  • legacy domains still maintained for existing accounts: @windowslive.com and @passport.com (and their regional variants). [1][2]

Notes:

  • Microsoft’s documentation treats @Hotmail, @LiVe, @msn and @passport addresses as supported legacy Microsoft account addresses but mainly issues new mailboxes on @outlook.com now. [1][2]
  • There are many country-specific TLD variants for Hotmail/Live/Outlook/MSN (too many to list exhaustively here). If you want, I can enumerate every known country-code variant and provide sources for each.

Sources: Microsoft Support (Outlook/Hotmail/MSN support) [1]; Outlook.com / Microsoft account pages and historical domain lists compiled (examples) [2][3].


Add missing @windowslive.com legacy domain to SUPPORTED_DOMAINS list.

Microsoft's documentation treats @windowslive.com as a supported legacy Microsoft account domain still maintained for existing accounts. The current list is missing windowslive.com and its country variants. Additionally, some country-specific variants like live.cn appear to be absent, though the list covers most common regional TLDs.

Add windowslive.com at minimum to ensure accounts migrated to or still using this legacy domain are properly recognized.

🤖 Prompt for AI Agents
In src/Emails/Canonicals/Providers/Outlook.php around lines 17 to 30, the
SUPPORTED_DOMAINS array omits the legacy Microsoft domain windowslive.com (and
country variants); add at least 'windowslive.com' to the list of supported
domains (ideally grouped with other Microsoft legacy domains like hotmail/live)
so addresses using the legacy domain are recognized; update any related tests or
validation that depend on SUPPORTED_DOMAINS to include this new entry.


private const CANONICAL_DOMAIN = 'outlook.com';
Expand All @@ -31,9 +41,13 @@ public function getCanonical(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);
// Remove plus addressing (subaddress) - everything after +
$normalizedLocal = $this->removePlusAddressing($normalizedLocal);

// Ensure local part is not empty after normalization
if (empty($normalizedLocal)) {
throw new \InvalidArgumentException('Email local part cannot be empty after normalization');
}

return [
'local' => $normalizedLocal,
Expand Down
16 changes: 3 additions & 13 deletions src/Emails/Canonicals/Providers/Protonmail.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
* ProtonMail
*
* Handles ProtonMail email normalization
* - TODO: Plus addressing and dots removal commented out until manual confirmation
* - Preserves dots and hyphens in local part
* - Preserves all characters in local part (no subaddress or dot removal)
* - Normalizes to protonmail.com domain
*/
class Protonmail extends Provider
Expand All @@ -28,17 +27,8 @@ public function getCanonical(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;

// 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);
// }
// ProtonMail doesn't remove subaddresses or dots
// Just normalize case and domain

return [
'local' => $normalizedLocal,
Expand Down
27 changes: 10 additions & 17 deletions src/Emails/Canonicals/Providers/Yahoo.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
/**
* Yahoo
*
* Handles Yahoo email normalization
* - TODO: Plus addressing, dots, and hyphens removal commented out until manual confirmation
* - Preserves dots and hyphens in local part
* Handles Yahoo email normalization based on validator.js rules
* - Removes hyphen-based subaddress (everything after last -)
* - Preserves dots in local part
* - Normalizes to yahoo.com domain
*/
class Yahoo extends Provider
{
private const SUPPORTED_DOMAINS = [
'yahoo.com', 'yahoo.co.uk', 'yahoo.ca',
'yahoo.com', 'yahoo.co.uk', 'yahoo.ca', 'yahoo.de', 'yahoo.fr', 'yahoo.in', 'yahoo.it',
'ymail.com', 'rocketmail.com',
];

Expand All @@ -31,20 +31,13 @@ public function getCanonical(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;
// Remove hyphen-based subaddress (everything after last -)
$normalizedLocal = $this->removeHyphenSubaddress($normalizedLocal);

// 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);
// Ensure local part is not empty after normalization
if (empty($normalizedLocal)) {
throw new \InvalidArgumentException('Email local part cannot be empty after normalization');
}
Comment on lines +34 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify test coverage for empty local part validation.

The implementation correctly validates that the local part is not empty after hyphen subaddress removal. However, I don't see test cases in YahooTest.php that verify this exception is thrown for edge cases like:

  • Single hyphen: -
  • Only hyphens: -- or ---

Run the following script to check if the exception scenario is tested:


🏁 Script executed:

#!/bin/bash
# Description: Search for test cases that verify the InvalidArgumentException

# Check if YahooTest.php tests the exception
rg -n "expectException|InvalidArgumentException|^\s*\['-'" tests/Canonicals/Providers/YahooTest.php

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Find test files related to Yahoo provider
fd -i yahoo tests/ --type f

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check tests directory structure
find tests/ -type d | head -20

Length of output: 66


🏁 Script executed:

#!/bin/bash
# Search for any test files in Canonicals/Providers directory
fd --type f tests/Canonicals/Providers/ 2>/dev/null || find tests/Canonicals/Providers/ -type f 2>/dev/null

Length of output: 382


🏁 Script executed:

#!/bin/bash
# Broader search for InvalidArgumentException in all test files
rg -n "InvalidArgumentException" tests/ --type php

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Display YahooTest.php content to see test cases
cat tests/Canonicals/Providers/YahooTest.php

Length of output: 4356


🏁 Script executed:

#!/bin/bash
# Display Yahoo.php implementation
cat src/Emails/Canonicals/Providers/Yahoo.php

Length of output: 1560


🏁 Script executed:

#!/bin/bash
# Search for test data or fixtures related to Yahoo tests
rg -n "dataProvider|@test" tests/Canonicals/Providers/YahooTest.php -A 2

Length of output: 0


Add test cases to verify InvalidArgumentException for empty local parts.

The implementation correctly throws InvalidArgumentException when the local part becomes empty after hyphen subaddress removal (lines 34-40 of Yahoo.php). However, the test suite lacks coverage for this exception scenario.

Current test cases do not include edge cases that would result in an empty local part after normalization:

  • Single hyphen: -
  • Only hyphens: --, ---, etc.

Add test cases to verify the exception is thrown for these inputs.

🤖 Prompt for AI Agents
In src/Emails/Canonicals/Providers/Yahoo.php around lines 34-40 the code throws
an InvalidArgumentException when the local part becomes empty after removing
hyphen subaddresses; the test suite lacks coverage for inputs that produce an
empty local part (e.g., '-', '--', '---'). Add test cases in the YahooTest
(test_get_canonical or a new test method) that assert InvalidArgumentException
is thrown for these inputs (use a data provider or iterate the cases) so the
exception path is exercised and covered by tests.


return [
'local' => $normalizedLocal,
Expand Down
50 changes: 50 additions & 0 deletions src/Emails/Canonicals/Providers/Yandex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Utopia\Emails\Canonicals\Providers;

use Utopia\Emails\Canonicals\Provider;

/**
* Yandex
*
* Handles Yandex email normalization based on validator.js rules
* - Preserves all characters in local part (no subaddress removal)
* - Normalizes to yandex.ru domain
*/
class Yandex extends Provider
{
private const SUPPORTED_DOMAINS = [
'yandex.ru', 'yandex.ua', 'yandex.kz', 'yandex.com', 'yandex.by', 'ya.ru',
];

private const CANONICAL_DOMAIN = 'yandex.ru';

public function supports(string $domain): bool
{
return in_array($domain, self::SUPPORTED_DOMAINS, true);
}

public function getCanonical(string $local, string $domain): array
{
// Convert to lowercase
$normalizedLocal = $this->toLowerCase($local);

// Yandex doesn't remove subaddresses or dots
// Just normalize case and domain

return [
'local' => $normalizedLocal,
'domain' => self::CANONICAL_DOMAIN,
];
}

public function getCanonicalDomain(): string
{
return self::CANONICAL_DOMAIN;
}

public function getSupportedDomains(): array
{
return self::SUPPORTED_DOMAINS;
}
}
37 changes: 19 additions & 18 deletions tests/Canonicals/Providers/FastmailTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,29 @@ public function test_supports(): void
public function test_get_canonical(): void
{
$testCases = [
// 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
// Fastmail preserves all characters (no subaddress or dot removal)
['user.name', 'fastmail.com', 'user.name', 'fastmail.com'],
['user.name+tag', 'fastmail.com', 'user.name+tag', 'fastmail.com'],
['user.name+spam', 'fastmail.com', 'user.name+spam', 'fastmail.com'],
['user.name+newsletter', 'fastmail.com', 'user.name+newsletter', 'fastmail.com'],
['user.name+work', 'fastmail.com', 'user.name+work', 'fastmail.com'],
['user.name+personal', 'fastmail.com', 'user.name+personal', 'fastmail.com'],
['user.name+test123', 'fastmail.com', 'user.name+test123', 'fastmail.com'],
['user.name+anything', 'fastmail.com', 'user.name+anything', 'fastmail.com'],
['user.name+verylongtag', 'fastmail.com', 'user.name+verylongtag', 'fastmail.com'],
['user.name+tag.with.dots', 'fastmail.com', 'user.name+tag.with.dots', 'fastmail.com'],
['user.name+tag-with-hyphens', 'fastmail.com', 'user.name+tag-with-hyphens', 'fastmail.com'],
['user.name+tag_with_underscores', 'fastmail.com', 'user.name+tag_with_underscores', 'fastmail.com'],
['user.name+tag123', 'fastmail.com', 'user.name+tag123', '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'],
['u.s.e.r.n.a.m.e+tag', 'fastmail.com', 'u.s.e.r.n.a.m.e+tag', 'fastmail.com'],
['user+', 'fastmail.com', 'user+', 'fastmail.com'],
['user.', 'fastmail.com', 'user.', 'fastmail.com'],
['.user', 'fastmail.com', '.user', 'fastmail.com'],
['user..name', 'fastmail.com', 'user..name', 'fastmail.com'],
// Other Fastmail domain
['user.name+tag', 'fastmail.fm', 'user.name+tag', 'fastmail.com'],
['user.name', 'fastmail.fm', 'user.name', 'fastmail.com'],
];

foreach ($testCases as [$inputLocal, $inputDomain, $expectedLocal, $expectedDomain]) {
Expand Down
Loading