1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Field;
6:
7: use Atk4\Data\Field;
8: use Atk4\Data\ValidationException;
9:
10: /**
11: * Stores valid email as per configuration.
12: *
13: * Usage:
14: * $user->addField('email', [EmailField::class]);
15: * $user->addField('email_mx_check', [EmailField::class, 'dnsCheck' => true]);
16: * $user->addField('email_with_name', [EmailField::class, 'allowName' => true]);
17: */
18: class EmailField extends Field
19: {
20: /** @var bool Enable lookup for MX record for email addresses stored */
21: public $dnsCheck = false;
22:
23: /** @var bool Allow display name as per RFC2822, eg. format like "Romans <me@example.com>" */
24: public $allowName = false;
25:
26: #[\Override]
27: public function normalize($value)
28: {
29: $value = parent::normalize($value);
30: if ($value === null) {
31: return $value;
32: }
33:
34: $email = trim($value);
35: if ($this->allowName) {
36: $email = preg_replace('~^[^<]*<([^>]*)>~', '\1', $email);
37: }
38:
39: if (!str_contains($email, '@')) {
40: throw new ValidationException([$this->shortName => 'Email address does not have domain'], $this->getOwner());
41: }
42:
43: [$user, $domain] = explode('@', $email, 2);
44: $domain = idn_to_ascii($domain, \IDNA_DEFAULT, \INTL_IDNA_VARIANT_UTS46); // always convert domain to ASCII
45:
46: if (!filter_var($user . '@' . $domain, \FILTER_VALIDATE_EMAIL)) {
47: throw new ValidationException([$this->shortName => 'Email address format is invalid'], $this->getOwner());
48: }
49:
50: if ($this->dnsCheck) {
51: if (!$this->hasAnyDnsRecord($domain)) {
52: throw new ValidationException([$this->shortName => 'Email address domain does not exist'], $this->getOwner());
53: }
54: }
55:
56: return parent::normalize($value);
57: }
58:
59: /**
60: * @param array<int, string> $types
61: */
62: private function hasAnyDnsRecord(string $domain, array $types = ['MX', 'A', 'AAAA', 'CNAME']): bool
63: {
64: foreach (array_unique(array_map('strtoupper', $types)) as $t) {
65: $dnsConsts = [
66: 'MX' => \DNS_MX,
67: 'A' => \DNS_A,
68: 'AAAA' => \DNS_AAAA,
69: 'CNAME' => \DNS_CNAME,
70: ];
71:
72: $records = @dns_get_record($domain . '.', $dnsConsts[$t]);
73: if ($records === false) { // retry once on failure
74: $records = dns_get_record($domain . '.', $dnsConsts[$t]);
75: }
76: if ($records !== false && count($records) > 0) {
77: return true;
78: }
79: }
80:
81: return false;
82: }
83: }
84: