- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
190 lines
5.2 KiB
PHP
190 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Core\ValueObjects;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
final readonly class EmailAddress
|
|
{
|
|
public function __construct(
|
|
private string $value
|
|
) {
|
|
$this->validate();
|
|
}
|
|
|
|
// Factory Methods
|
|
public static function from(string $email): self
|
|
{
|
|
return new self($email);
|
|
}
|
|
|
|
public static function parse(string $input): self
|
|
{
|
|
$email = trim($input);
|
|
|
|
// Remove surrounding quotes if present
|
|
if (str_starts_with($email, '"') && str_ends_with($email, '"')) {
|
|
$email = substr($email, 1, -1);
|
|
}
|
|
|
|
// Extract email from "Name <email@domain.com>" format
|
|
if (preg_match('/<([^>]+)>/', $email, $matches)) {
|
|
$email = $matches[1];
|
|
}
|
|
|
|
return new self($email);
|
|
}
|
|
|
|
// Validation
|
|
public static function isValid(string $email): bool
|
|
{
|
|
if (empty($email) || strlen($email) > 320) { // RFC 5321 limit
|
|
return false;
|
|
}
|
|
|
|
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
return false;
|
|
}
|
|
|
|
// Additional domain validation
|
|
$domain = substr(strrchr($email, '@'), 1);
|
|
|
|
return strlen($domain) >= 4 && str_contains($domain, '.');
|
|
}
|
|
|
|
// Getters
|
|
public function getValue(): string
|
|
{
|
|
return $this->value;
|
|
}
|
|
|
|
public function getLocalPart(): string
|
|
{
|
|
return substr($this->value, 0, strpos($this->value, '@'));
|
|
}
|
|
|
|
public function getDomain(): string
|
|
{
|
|
return substr($this->value, strpos($this->value, '@') + 1);
|
|
}
|
|
|
|
// Domain checks
|
|
public function isDisposable(): bool
|
|
{
|
|
$disposableDomains = [
|
|
'10minutemail.com', 'tempmail.org', 'guerrillamail.com',
|
|
'mailinator.com', 'throwaway.email', 'temp-mail.org',
|
|
];
|
|
|
|
return in_array(strtolower($this->getDomain()), $disposableDomains, true);
|
|
}
|
|
|
|
public function isCommonProvider(): bool
|
|
{
|
|
$commonProviders = [
|
|
'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
|
|
'web.de', 'gmx.de', 't-online.de', 'freenet.de',
|
|
];
|
|
|
|
return in_array(strtolower($this->getDomain()), $commonProviders, true);
|
|
}
|
|
|
|
public function isCorporate(): bool
|
|
{
|
|
return ! $this->isCommonProvider() && ! $this->isDisposable();
|
|
}
|
|
|
|
// Formatting
|
|
public function normalize(): self
|
|
{
|
|
$normalized = strtolower(trim($this->value));
|
|
|
|
// Gmail: remove dots from local part and ignore everything after +
|
|
if (str_ends_with($normalized, '@gmail.com')) {
|
|
$local = $this->getLocalPart();
|
|
$local = str_replace('.', '', $local);
|
|
if (str_contains($local, '+')) {
|
|
$local = substr($local, 0, strpos($local, '+'));
|
|
}
|
|
$normalized = $local . '@gmail.com';
|
|
}
|
|
|
|
return new self($normalized);
|
|
}
|
|
|
|
public function obfuscate(): string
|
|
{
|
|
$local = $this->getLocalPart();
|
|
$domain = $this->getDomain();
|
|
|
|
if (strlen($local) <= 2) {
|
|
$obfuscatedLocal = str_repeat('*', strlen($local));
|
|
} else {
|
|
$obfuscatedLocal = $local[0] . str_repeat('*', strlen($local) - 2) . $local[-1];
|
|
}
|
|
|
|
$domainParts = explode('.', $domain);
|
|
if (count($domainParts) >= 2) {
|
|
$tld = array_pop($domainParts);
|
|
$mainDomain = array_pop($domainParts);
|
|
$obfuscatedDomain = substr($mainDomain, 0, 1) . str_repeat('*', strlen($mainDomain) - 1) . '.' . $tld;
|
|
} else {
|
|
$obfuscatedDomain = str_repeat('*', strlen($domain));
|
|
}
|
|
|
|
return $obfuscatedLocal . '@' . $obfuscatedDomain;
|
|
}
|
|
|
|
// Comparison
|
|
public function equals(EmailAddress $other): bool
|
|
{
|
|
return $this->normalize()->value === $other->normalize()->value;
|
|
}
|
|
|
|
public function isSameDomain(EmailAddress $other): bool
|
|
{
|
|
return strtolower($this->getDomain()) === strtolower($other->getDomain());
|
|
}
|
|
|
|
// String representation
|
|
public function toString(): string
|
|
{
|
|
return $this->value;
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
return $this->value;
|
|
}
|
|
|
|
// Validation
|
|
private function validate(): void
|
|
{
|
|
if (empty($this->value)) {
|
|
throw new InvalidArgumentException('Email address cannot be empty');
|
|
}
|
|
|
|
if (strlen($this->value) > 320) { // RFC 5321 limit
|
|
throw new InvalidArgumentException('Email address too long (max 320 characters)');
|
|
}
|
|
|
|
if (! filter_var($this->value, FILTER_VALIDATE_EMAIL)) {
|
|
throw new InvalidArgumentException("Invalid email address: {$this->value}");
|
|
}
|
|
|
|
// Additional domain validation
|
|
$domain = $this->getDomain();
|
|
if (strlen($domain) < 4 || ! str_contains($domain, '.')) {
|
|
throw new InvalidArgumentException("Invalid domain in email address: {$domain}");
|
|
}
|
|
|
|
// Check for valid local part length (RFC 5321)
|
|
$localPart = $this->getLocalPart();
|
|
if (strlen($localPart) > 64) {
|
|
throw new InvalidArgumentException('Email local part too long (max 64 characters)');
|
|
}
|
|
}
|
|
}
|