Skip to content

Commit 0a6a373

Browse files
committed
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.
1 parent 73f5345 commit 0a6a373

File tree

20 files changed

+1658
-72
lines changed

20 files changed

+1658
-72
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Utopia Emails
22

3-
[![Build Status](https://travis-ci.org/utopia-php/emails.svg?branch=master)](https://travis-ci.com/utopia-php/emails)
3+
[![Tests](https://github.com/utopia-php/emails/workflows/Tests/badge.svg)](https://github.com/utopia-php/emails/actions/workflows/test.yml)
4+
[![Linter](https://github.com/utopia-php/emails/workflows/Linter/badge.svg)](https://github.com/utopia-php/emails/actions/workflows/linter.yml)
5+
[![CodeQL](https://github.com/utopia-php/emails/workflows/CodeQL/badge.svg)](https://github.com/utopia-php/emails/actions/workflows/codeql-analysis.yml)
46
![Total Downloads](https://img.shields.io/packagist/dt/utopia-php/emails.svg)
57
[![Discord](https://img.shields.io/discord/564160730845151244)](https://appwrite.io/discord)
68

composer.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
{
99
"name": "Eldad Fux",
1010
"email": "[email protected]"
11-
},
12-
{
13-
"name": "Wess Cope",
14-
"email": "[email protected]"
1511
}
1612
],
1713
"autoload": {

data/sources.php

Lines changed: 0 additions & 67 deletions
This file was deleted.

src/Emails/Email.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
namespace Utopia\Emails;
44

55
use Exception;
6+
use Utopia\Emails\Normalizer\Provider;
7+
use Utopia\Emails\Normalizer\Providers\Fastmail;
8+
use Utopia\Emails\Normalizer\Providers\Generic;
9+
use Utopia\Emails\Normalizer\Providers\Gmail;
10+
use Utopia\Emails\Normalizer\Providers\Icloud;
11+
use Utopia\Emails\Normalizer\Providers\Outlook;
12+
use Utopia\Emails\Normalizer\Providers\Protonmail;
13+
use Utopia\Emails\Normalizer\Providers\Yahoo;
614

715
class Email
816
{
@@ -58,6 +66,13 @@ class Email
5866
*/
5967
protected static $disposableDomains = null;
6068

69+
/**
70+
* Email providers
71+
*
72+
* @var Provider[]
73+
*/
74+
protected static $providers = null;
75+
6176
/**
6277
* Email constructor.
6378
*/
@@ -268,6 +283,93 @@ public function normalize(): string
268283
return $this->email;
269284
}
270285

286+
/**
287+
* Get unique email address by removing aliases and provider-specific variations
288+
* This method removes plus addressing, dot notation (for Gmail), and other aliasing techniques
289+
* to return the canonical form of the email address
290+
*/
291+
public function getUnique(): string
292+
{
293+
$provider = $this->getProviderForDomain($this->domain);
294+
$normalized = $provider->normalize($this->local, $this->domain);
295+
296+
return $normalized['local'].'@'.$normalized['domain'];
297+
}
298+
299+
/**
300+
* Check if the email domain is supported for normalization
301+
*/
302+
public function isNormalizationSupported(): bool
303+
{
304+
return $this->isDomainSupported($this->domain);
305+
}
306+
307+
/**
308+
* Get the canonical domain for this email
309+
*/
310+
public function getCanonicalDomain(): ?string
311+
{
312+
$provider = $this->getProviderForDomain($this->domain);
313+
314+
// Only return canonical domain if it's not the generic provider
315+
if (! $provider instanceof Generic) {
316+
return $provider->getCanonicalDomain();
317+
}
318+
319+
return null;
320+
}
321+
322+
/**
323+
* Get the appropriate provider for a given domain
324+
*/
325+
protected function getProviderForDomain(string $domain): Provider
326+
{
327+
if (self::$providers === null) {
328+
self::$providers = [
329+
new Gmail,
330+
new Outlook,
331+
new Yahoo,
332+
new Icloud,
333+
new Protonmail,
334+
new Fastmail,
335+
];
336+
}
337+
338+
foreach (self::$providers as $provider) {
339+
if ($provider->supports($domain)) {
340+
return $provider;
341+
}
342+
}
343+
344+
// Return generic provider if no specific provider found
345+
return new Generic;
346+
}
347+
348+
/**
349+
* Check if a domain is supported by any provider
350+
*/
351+
protected function isDomainSupported(string $domain): bool
352+
{
353+
if (self::$providers === null) {
354+
self::$providers = [
355+
new Gmail,
356+
new Outlook,
357+
new Yahoo,
358+
new Icloud,
359+
new Protonmail,
360+
new Fastmail,
361+
];
362+
}
363+
364+
foreach (self::$providers as $provider) {
365+
if ($provider->supports($domain)) {
366+
return true;
367+
}
368+
}
369+
370+
return false;
371+
}
372+
271373
/**
272374
* Get email in different formats
273375
*/

src/Emails/Normalizer/Provider.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Utopia\Emails\Normalizer;
4+
5+
/**
6+
* Abstract Email Provider
7+
*
8+
* Base class for email provider normalization
9+
*/
10+
abstract class Provider
11+
{
12+
/**
13+
* Check if this provider supports the given domain
14+
*/
15+
abstract public function supports(string $domain): bool;
16+
17+
/**
18+
* Normalize the email address according to provider rules
19+
*
20+
* @param string $local The local part of the email (before @)
21+
* @param string $domain The domain part of the email (after @)
22+
* @return array Array with 'local' and 'domain' keys containing normalized values
23+
*/
24+
abstract public function normalize(string $local, string $domain): array;
25+
26+
/**
27+
* Get the canonical domain for this provider
28+
*/
29+
abstract public function getCanonicalDomain(): string;
30+
31+
/**
32+
* Get all supported domains for this provider
33+
*/
34+
abstract public function getSupportedDomains(): array;
35+
36+
/**
37+
* Remove plus addressing from local part
38+
*/
39+
protected function removePlusAddressing(string $local): string
40+
{
41+
$plusPos = strpos($local, '+');
42+
if ($plusPos !== false && $plusPos > 0) {
43+
return substr($local, 0, $plusPos);
44+
}
45+
46+
return $local;
47+
}
48+
49+
/**
50+
* Remove all dots from local part
51+
*/
52+
protected function removeDots(string $local): string
53+
{
54+
return str_replace('.', '', $local);
55+
}
56+
57+
/**
58+
* Remove all hyphens from local part
59+
*/
60+
protected function removeHyphens(string $local): string
61+
{
62+
return str_replace('-', '', $local);
63+
}
64+
65+
/**
66+
* Convert local part to lowercase
67+
*/
68+
protected function toLowerCase(string $local): string
69+
{
70+
return strtolower($local);
71+
}
72+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Utopia\Emails\Normalizer\Providers;
4+
5+
use Utopia\Emails\Normalizer\Provider;
6+
7+
/**
8+
* Fastmail
9+
*
10+
* Handles Fastmail email normalization
11+
* - Removes plus addressing
12+
* - Preserves dots and hyphens in local part
13+
* - Normalizes to fastmail.com domain
14+
*/
15+
class Fastmail extends Provider
16+
{
17+
private const SUPPORTED_DOMAINS = ['fastmail.com', 'fastmail.fm'];
18+
19+
private const CANONICAL_DOMAIN = 'fastmail.com';
20+
21+
public function supports(string $domain): bool
22+
{
23+
return in_array($domain, self::SUPPORTED_DOMAINS, true);
24+
}
25+
26+
public function normalize(string $local, string $domain): array
27+
{
28+
// Convert to lowercase
29+
$normalizedLocal = $this->toLowerCase($local);
30+
31+
// Check if there's plus addressing
32+
$hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0;
33+
34+
// Remove plus addressing (everything after +)
35+
$normalizedLocal = $this->removePlusAddressing($normalizedLocal);
36+
37+
// Remove dots only if there was plus addressing (Fastmail treats dots as aliases only with plus)
38+
if ($hasPlus) {
39+
$normalizedLocal = $this->removeDots($normalizedLocal);
40+
}
41+
42+
return [
43+
'local' => $normalizedLocal,
44+
'domain' => self::CANONICAL_DOMAIN,
45+
];
46+
}
47+
48+
public function getCanonicalDomain(): string
49+
{
50+
return self::CANONICAL_DOMAIN;
51+
}
52+
53+
public function getSupportedDomains(): array
54+
{
55+
return self::SUPPORTED_DOMAINS;
56+
}
57+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Utopia\Emails\Normalizer\Providers;
4+
5+
use Utopia\Emails\Normalizer\Provider;
6+
7+
/**
8+
* Generic
9+
*
10+
* Handles generic email normalization for unsupported providers
11+
* - Only removes plus addressing
12+
* - Preserves all other characters
13+
*/
14+
class Generic extends Provider
15+
{
16+
public function supports(string $domain): bool
17+
{
18+
// Generic provider supports all domains
19+
return true;
20+
}
21+
22+
public function normalize(string $local, string $domain): array
23+
{
24+
// Convert to lowercase
25+
$normalizedLocal = $this->toLowerCase($local);
26+
27+
// Check if there's plus addressing
28+
$hasPlus = strpos($normalizedLocal, '+') !== false && strpos($normalizedLocal, '+') > 0;
29+
30+
// Remove plus addressing (everything after +)
31+
$normalizedLocal = $this->removePlusAddressing($normalizedLocal);
32+
33+
// Remove dots and hyphens only if there was plus addressing (generic providers treat these as aliases only with plus)
34+
if ($hasPlus) {
35+
$normalizedLocal = $this->removeDots($normalizedLocal);
36+
$normalizedLocal = $this->removeHyphens($normalizedLocal);
37+
}
38+
39+
return [
40+
'local' => $normalizedLocal,
41+
'domain' => $domain,
42+
];
43+
}
44+
45+
public function getCanonicalDomain(): string
46+
{
47+
// Generic provider doesn't have a canonical domain
48+
return '';
49+
}
50+
51+
public function getSupportedDomains(): array
52+
{
53+
// Generic provider supports all domains
54+
return [];
55+
}
56+
}

0 commit comments

Comments
 (0)