ILIAS  trunk Revision v12.0_alpha-16-g3e876e53c80
EmailAddress.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\Data;
22
41{
42 protected string $addressFull;
43 protected string $domainPart;
44 protected string $localPart;
45 protected bool $isAscii;
46 protected bool $isDomainPartAscii;
47 protected bool $isLocalPartAscii;
48
49
50 public function __construct(string $address_Full)
51 {
52 $this->addressFull = $this->digestFullAddress($address_Full);
53 $this->domainPart = $this->digestDomainPart($address_Full);
54 $this->localPart = $this->digestLocalPart($address_Full);
55 if ($this->isDomainPartAscii && $this->isLocalPartAscii) {
56 $this->isAscii = true;
57 } else {
58 $this->isAscii = false;
59 }
60 }
61
62 public function getAddressFull(): string
63 {
64 return $this->addressFull;
65 }
66
67 public function getDomainPart(): string
68 {
69 return $this->domainPart;
70 }
71
72 public function getLocalPart(): string
73 {
74 return $this->localPart;
75 }
76
77 public function getIsAscii(): bool
78 {
79 return $this->isAscii;
80 }
81
82 public function getIsDomainPartAscii(): bool
83 {
85 }
86
87 public function getIsLocalPartAscii(): bool
88 {
90 }
91
92 public function __toString(): string
93 {
94 return $this->getAddressFull();
95 }
96
97 protected function checkAscii(string $str): bool
98 {
99 return mb_check_encoding($str, 'ASCII');
100 }
101
102 protected function digestFullAddress(string $address): string
103 {
104 $address = trim($address);
105
106 if (substr_count($address, '@') !== 1) {
107 throw new \InvalidArgumentException("Email must contain exactly one '@' character.");
108 }
109
110 [$local, $domain] = explode('@', $address, 2);
111
112 if ($local === '' || $domain === '') {
113 throw new \InvalidArgumentException("Email must have non-empty local and domain parts.");
114 }
115
116 $this->addressFull = $address;
117 return $address;
118 }
119
120 protected function digestDomainPart(string $address): string
121 {
122 [, $domain] = explode('@', $address, 2);
123
124 $this->isDomainPartAscii = $this->checkAscii($domain);
125
126 if ($domain === 'localhost') {
127 return $domain;
128 }
129
130 if (preg_match('/[\p{C}\p{Z}]/u', $domain)) {
131 throw new \InvalidArgumentException("Domain part contains invalid characters (e.g., whitespace or control).");
132 }
133
134 if (str_contains($domain, '..')) {
135 throw new \InvalidArgumentException("Domain part contains consecutive dots.");
136 }
137
138 // not flagging double hyphens as punycode uses those
139
140 if (substr_count($domain, '.') < 1) {
141 throw new \InvalidArgumentException("Domain must contain at least one dot except for 'localhost'.");
142 }
143
144 if (strlen($domain) > 254) {
145 throw new \InvalidArgumentException("Domain part exceeds 254 character limit.");
146 }
147
148 $labels = explode('.', $domain);
149 foreach ($labels as $label) {
150 if (strlen($label) > 63) {
151 throw new \InvalidArgumentException("Domain label exceeds 63 character limit.");
152 }
153
154 if ($label === '') {
155 throw new \InvalidArgumentException("Domain contains an empty label.");
156 }
157
158 if ($label[0] === '-' || $label[strlen($label) - 1] === '-') {
159 throw new \InvalidArgumentException("Domain labels must not start or end with a hyphen.");
160 }
161 }
162
163 return $domain;
164 }
165
166 protected function digestLocalPart(string $address): string
167 {
168 [$local,] = explode('@', $address, 2);
169
170 if (!mb_check_encoding($local, 'UTF-8')) {
171 throw new \InvalidArgumentException("Local part is not valid UTF-8.");
172 }
173
174 if ($local[0] === '.' || str_ends_with($local, '.')) {
175 throw new \InvalidArgumentException("Local part cannot start or end with a dot.");
176 }
177
178 if (str_contains($local, '..')) {
179 throw new \InvalidArgumentException("Local part cannot contain consecutive dots.");
180 }
181
182 // double quotes are allowed only as first and last character
183 if (str_starts_with($local, '"') && str_ends_with($local, '"')) {
184 $local_strip_quotes = substr($local, 1, -1);
185 } else {
186 $local_strip_quotes = $local;
187 }
188
189 if (preg_match('/[\x00-\x1F\x7F]/', $local_strip_quotes)) {
190 throw new \InvalidArgumentException("Local part contains unsupported control characters or invalid escape sequences.");
191 }
192
193 // check if safe Ascii
194 if (preg_match('/^[a-zA-Z0-9!#$%&\'*+\/=?^_`{|}~\.-]+$/', $local_strip_quotes)) {
195 $this->isLocalPartAscii = true;
196 } else {
197 $this->isLocalPartAscii = false;
198 }
199 // check if save Unicode
200 if (!self::isAllowedIntlUnicode($local_strip_quotes)) {
201 throw new \InvalidArgumentException("Local part is not a valid Unicode string.");
202 }
203
204 return $local;
205 }
206
207 protected static function isAllowedIntlUnicode(string $string): bool
208 {
209 // Check allowed Unicode characters this includes e.g.
210 // * a-z, A-Z, 0-9
211 // * Latin accented: é, ñ, ö, č
212 // * Greek: Α, β, Ω
213 // * Cyrillic: Б, и, я
214 // * Arabic letters: ا, ب, خ
215 // * Hebrew: א, ב, ג
216 // * East Asian ideographs (CJK): 日, 本, 語, 汉, 字
217 // * Devanagari (Hindi, Marathi): अ, आ, क
218 // * Hangul (Korean): 한, 글
219 // * and more
220 // \p{L} includes all characters Unicode defines as letters
221 // \p{N} includes all characters Unicode defines as numbers
222 // this excludes emojis and control characters which are not allowed in the local part
223 if (!preg_match('/^[\p{L}\p{N}!#$%&\'*+\/=?^_`{|}~\.-]+$/u', $string)) {
224 return false;
225 } else {
226 return true;
227 }
228 }
229}
An Email Address is a common personal address for people online.
digestDomainPart(string $address)
digestFullAddress(string $address)
__construct(string $address_Full)
static isAllowedIntlUnicode(string $string)
digestLocalPart(string $address)