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