Files
michaelschiemer/src/Framework/Core/ValueObjects/EmailAddress.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

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)');
}
}
}