‪TYPO3CMS  ‪main
Rfc822AddressesParser.php
Go to the documentation of this file.
1 <?php
2 
3 namespace ‪TYPO3\CMS\Core\Mail;
4 
67 {
73  private ‪$address = '';
74 
80  private ‪$default_domain = 'localhost';
81 
87  private ‪$validate = true;
88 
94  private ‪$addresses = [];
95 
101  private ‪$structure = [];
102 
108  private ‪$error;
109 
115  private ‪$index;
116 
122  private ‪$num_groups = 0;
123 
129  private ‪$limit;
130 
139  public function ‪__construct(‪$address = null, ‪$default_domain = null, ‪$validate = null, ‪$limit = null)
140  {
141  if (isset(‪$address)) {
142  $this->address = ‪$address;
143  }
144  if (isset(‪$default_domain)) {
145  $this->default_domain = ‪$default_domain;
146  }
147  if (isset(‪$validate)) {
148  $this->validate = ‪$validate;
149  }
150  if (isset(‪$limit)) {
151  $this->limit = ‪$limit;
152  }
153  }
154 
165  public function ‪parseAddressList(‪$address = null, ‪$default_domain = null, ‪$validate = null, ‪$limit = null)
166  {
167  if (isset(‪$address)) {
168  $this->address = ‪$address;
169  }
170  if (isset(‪$default_domain)) {
171  $this->default_domain = ‪$default_domain;
172  }
173  if (isset(‪$validate)) {
174  $this->validate = ‪$validate;
175  }
176  if (isset(‪$limit)) {
177  $this->limit = ‪$limit;
178  }
179  $this->structure = [];
180  $this->addresses = [];
181  $this->error = null;
182  $this->index = null;
183  // Unfold any long lines in $this->address.
184  $this->address = (string)preg_replace('/\\r?\\n/', '
185 ', (string)‪$this->address);
186  $this->address = (string)preg_replace('/\\r\\n(\\t| )+/', ' ', $this->address);
187  while ($this->address = $this->‪_splitAddresses($this->address)) {
188  }
189  if ($this->address === false || isset($this->error)) {
190  throw new \InvalidArgumentException((string)$this->error, 1294681466);
191  }
192  // Validate each address individually. If we encounter an invalid
193  // address, stop iterating and return an error immediately.
194  foreach ($this->addresses as ‪$address) {
195  $valid = $this->‪_validateAddress($address);
196  if ($valid === false || isset($this->error)) {
197  throw new \InvalidArgumentException((string)$this->error, 1294681467);
198  }
199  $this->structure = array_merge($this->structure, $valid);
200  }
201  return ‪$this->structure;
202  }
203 
211  protected function ‪_splitAddresses(‪$address)
212  {
213  if (‪$address === false) {
214  return false;
215  }
216  $split_char = '';
217  $is_group = false;
218  if (!empty($this->limit) && count($this->addresses) == $this->limit) {
219  return '';
220  }
221  if ($this->‪_isGroup($address) && !isset($this->error)) {
222  $split_char = ';';
223  $is_group = true;
224  } elseif (!isset($this->error)) {
225  $split_char = ',';
226  $is_group = false;
227  } elseif (isset($this->error)) {
228  return false;
229  }
230  if ($split_char === '') {
231  return false;
232  }
233  // Split the string based on the above ten or so lines.
234  $parts = explode($split_char, ‪$address);
235  $string = $this->‪_splitCheck($parts, $split_char);
236  // If a group...
237  if ($is_group) {
238  // If $string does not contain a colon outside of
239  // brackets/quotes etc then something's fubar.
240  // First check there's a colon at all:
241  if (!str_contains($string, ':')) {
242  $this->error = 'Invalid address: ' . $string;
243  return false;
244  }
245  // Now check it's outside of brackets/quotes:
246  if (!$this->‪_splitCheck(explode(':', $string), ':')) {
247  return false;
248  }
249  // We must have a group at this point, so increase the counter:
250  $this->num_groups++;
251  }
252  // $string now contains the first full address/group.
253  // Add to the addresses array.
254  $this->addresses[] = [
255  'address' => trim($string),
256  'group' => $is_group,
257  ];
258  // Remove the now stored address from the initial line, the +1
259  // is to account for the explode character.
260  ‪$address = trim(substr(‪$address, strlen($string) + 1));
261  // If the next char is a comma and this was a group, then
262  // there are more addresses, otherwise, if there are any more
263  // chars, then there is another address.
264  if ($is_group && ‪$address[0] === ',') {
265  ‪$address = trim(substr(‪$address, 1));
266  return ‪$address;
267  }
268  if (‪$address !== '') {
269  return ‪$address;
270  }
271  return '';
272  }
273 
281  protected function ‪_isGroup(‪$address)
282  {
283  if (‪$address === false) {
284  return false;
285  }
286  // First comma not in quotes, angles or escaped:
287  $parts = explode(',', ‪$address);
288  $string = $this->‪_splitCheck($parts, ',');
289  // Now we have the first address, we can reliably check for a
290  // group by searching for a colon that's not escaped or in
291  // quotes or angle brackets.
292  if (count($parts = explode(':', $string)) > 1) {
293  $string2 = $this->‪_splitCheck($parts, ':');
294  return $string2 !== $string;
295  }
296  return false;
297  }
298 
307  protected function ‪_splitCheck($parts, $char)
308  {
309  $string = $parts[0];
310  $partsCounter = count($parts);
311  for ($i = 0; $i < $partsCounter; $i++) {
312  if ($this->‪_hasUnclosedQuotes($string) || $this->‪_hasUnclosedBrackets($string, '<>') || $this->‪_hasUnclosedBrackets($string, '[]') || $this->‪_hasUnclosedBrackets($string, '()') || substr($string, -1) === '\\') {
313  if (isset($parts[$i + 1])) {
314  $string = $string . $char . $parts[$i + 1];
315  } else {
316  $this->error = 'Invalid address spec. Unclosed bracket or quotes';
317  return false;
318  }
319  } else {
320  $this->index = $i;
321  break;
322  }
323  }
324  return $string;
325  }
326 
334  protected function ‪_hasUnclosedQuotes($string)
335  {
336  $string = trim($string);
337  $iMax = strlen($string);
338  $in_quote = false;
339  $i = ($slashes = 0);
340  for (; $i < $iMax; ++$i) {
341  switch ($string[$i]) {
342  case '\\':
343  ++$slashes;
344  break;
345  case '"':
346  if ($slashes % 2 == 0) {
347  $in_quote = !$in_quote;
348  }
349  // no break
350  default:
351  $slashes = 0;
352  }
353  }
354  return $in_quote;
355  }
356 
366  protected function ‪_hasUnclosedBrackets($string, $chars)
367  {
368  $num_angle_start = substr_count($string, $chars[0]);
369  $num_angle_end = substr_count($string, $chars[1]);
370  $this->‪_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
371  $this->‪_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
372  if ($num_angle_start < $num_angle_end) {
373  $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
374  return false;
375  }
376  return $num_angle_start > $num_angle_end;
377  }
378 
388  protected function ‪_hasUnclosedBracketsSub($string, &$num, $char)
389  {
390  if ($char === '') {
391  return $num;
392  }
393  $parts = explode($char, $string);
394  $partsCounter = count($parts);
395  for ($i = 0; $i < $partsCounter; $i++) {
396  if (substr($parts[$i], -1) === '\\' || $this->‪_hasUnclosedQuotes($parts[$i])) {
397  $num--;
398  }
399  if (isset($parts[$i + 1])) {
400  $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
401  }
402  }
403  return $num;
404  }
405 
413  protected function ‪_validateAddress(‪$address)
414  {
415  ‪$structure = [];
416  $is_group = false;
417  ‪$addresses = [];
418  if (‪$address['group']) {
419  $is_group = true;
420  // Get the group part of the name
421  $parts = explode(':', ‪$address['address']);
422  $groupname = $this->‪_splitCheck($parts, ':');
423  ‪$structure = [];
424  // And validate the group part of the name.
425  if (!$this->‪_validatePhrase($groupname)) {
426  $this->error = 'Group name did not validate.';
427  return false;
428  }
429  ‪$address['address'] = ltrim(substr(‪$address['address'], strlen($groupname . ':')));
430  }
431  // If a group then split on comma and put into an array.
432  // Otherwise, Just put the whole address in an array.
433  if ($is_group) {
434  while (‪$address['address'] !== '') {
435  $parts = explode(',', ‪$address['address']);
436  ‪$addresses[] = $this->‪_splitCheck($parts, ',');
437  ‪$address['address'] = trim(substr(‪$address['address'], strlen(end(‪$addresses) . ',')));
438  }
439  } else {
440  ‪$addresses[] = ‪$address['address'];
441  }
442  // Check that $addresses is set, if address like this:
443  // Groupname:;
444  // Then errors were appearing.
445  if (empty(‪$addresses)) {
446  $this->error = 'Empty group.';
447  return false;
448  }
449  // Trim the whitespace from all of the address strings.
450  array_map(trim(...), ‪$addresses);
451  // Validate each mailbox.
452  // Format could be one of: name <geezer@domain.com>
453  // geezer@domain.com
454  // geezer
455  // ... or any other format valid by RFC 822.
456  $addressesCount = count(‪$addresses);
457  for ($i = 0; $i < $addressesCount; $i++) {
458  if (!$this->‪validateMailbox(‪$addresses[$i])) {
459  if (empty($this->error)) {
460  $this->error = 'Validation failed for: ' . ‪$addresses[$i];
461  }
462  return false;
463  }
464  }
465  if ($is_group) {
466  ‪$structure = array_merge(‪$structure, ‪$addresses);
467  } else {
469  }
470  return ‪$structure;
471  }
472 
480  protected function ‪_validatePhrase($phrase)
481  {
482  // Splits on one or more Tab or space.
483  $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
484  $phrase_parts = [];
485  while (!empty($parts)) {
486  $phrase_parts[] = $this->‪_splitCheck($parts, ' ');
487  for ($i = 0; $i < $this->index + 1; $i++) {
488  array_shift($parts);
489  }
490  }
491  foreach ($phrase_parts as $part) {
492  // If quoted string:
493  if ($part[0] === '"') {
494  if (!$this->‪_validateQuotedString($part)) {
495  return false;
496  }
497  continue;
498  }
499  // Otherwise it's an atom:
500  if (!$this->‪_validateAtom($part)) {
501  return false;
502  }
503  }
504  return true;
505  }
506 
520  protected function ‪_validateAtom($atom)
521  {
522  if (!$this->validate) {
523  // Validation has been turned off; assume the atom is okay.
524  return true;
525  }
526  // Check for any char from ASCII 0 - ASCII 127
527  if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
528  return false;
529  }
530  // Check for specials:
531  if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
532  return false;
533  }
534  // Check for control characters (ASCII 0-31):
535  if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
536  return false;
537  }
538  return true;
539  }
540 
549  protected function ‪_validateQuotedString($qstring)
550  {
551  // Leading and trailing "
552  $qstring = (string)substr($qstring, 1, -1);
553  // Perform check, removing quoted characters first.
554  return !preg_match('/[\\x0D\\\\"]/', (string)preg_replace('/\\\\./', '', $qstring));
555  }
556 
565  protected function ‪validateMailbox(&$mailbox)
566  {
567  $route_addr = null;
568  $addr_spec = [];
569  // A couple of defaults.
570  $phrase = '';
571  $comments = [];
572  // Catch any RFC822 comments and store them separately.
573  $_mailbox = $mailbox;
574  while (trim($_mailbox) !== '') {
575  $parts = explode('(', $_mailbox);
576  $before_comment = $this->‪_splitCheck($parts, '(');
577  if ($before_comment != $_mailbox) {
578  // First char should be a (.
579  $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
580  $parts = explode(')', $comment);
581  $comment = $this->‪_splitCheck($parts, ')');
582  $comments[] = $comment;
583  // +2 is for the brackets
584  $_mailbox = substr($_mailbox, strpos($_mailbox, '(' . $comment) + strlen($comment) + 2);
585  } else {
586  break;
587  }
588  }
589  foreach ($comments as $comment) {
590  $mailbox = str_replace('(' . $comment . ')', '', $mailbox);
591  }
592  $mailbox = trim($mailbox);
593  // Check for name + route-addr
594  if (substr($mailbox, -1) === '>' && $mailbox[0] !== '<') {
595  $parts = explode('<', $mailbox);
596  $name = $this->‪_splitCheck($parts, '<');
597  $phrase = trim($name);
598  $route_addr = trim(substr($mailbox, strlen($name . '<'), -1));
599  if ($this->‪_validatePhrase($phrase) === false || ($route_addr = $this->‪_validateRouteAddr($route_addr)) === false) {
600  return false;
601  }
602  } else {
603  // First snip angle brackets if present.
604  if ($mailbox[0] === '<' && substr($mailbox, -1) === '>') {
605  $addr_spec = substr($mailbox, 1, -1);
606  } else {
607  $addr_spec = $mailbox;
608  }
609  if (($addr_spec = $this->‪_validateAddrSpec($addr_spec)) === false) {
610  return false;
611  }
612  }
613  // Construct the object that will be returned.
614  $mbox = new \stdClass();
615  // Add the phrase (even if empty) and comments
616  $mbox->personal = $phrase;
617  $mbox->comment = $comments ?? [];
618  if (isset($route_addr)) {
619  $mbox->mailbox = $route_addr['local_part'];
620  $mbox->host = $route_addr['domain'];
621  $route_addr['adl'] !== '' ? ($mbox->adl = $route_addr['adl']) : '';
622  } else {
623  $mbox->mailbox = $addr_spec['local_part'];
624  $mbox->host = $addr_spec['domain'];
625  }
626  $mailbox = $mbox;
627  return true;
628  }
629 
641  protected function ‪_validateRouteAddr($route_addr)
642  {
643  $return = [];
644  // Check for colon.
645  if (str_contains($route_addr, ':')) {
646  $parts = explode(':', $route_addr);
647  $route = $this->‪_splitCheck($parts, ':');
648  } else {
649  $route = $route_addr;
650  }
651  // If $route is same as $route_addr then the colon was in
652  // quotes or brackets or, of course, non existent.
653  if ($route === $route_addr) {
654  $route = '';
655  $addr_spec = $route_addr;
656  if (($addr_spec = $this->‪_validateAddrSpec($addr_spec)) === false) {
657  return false;
658  }
659  } else {
660  // Validate route part.
661  if (($route = $this->‪_validateRoute($route)) === false) {
662  return false;
663  }
664  $addr_spec = substr($route_addr, strlen($route . ':'));
665  // Validate addr-spec part.
666  if (($addr_spec = $this->‪_validateAddrSpec($addr_spec)) === false) {
667  return false;
668  }
669  }
670  $return['adl'] = $route;
671  $return = array_merge($return, $addr_spec);
672  return $return;
673  }
674 
683  protected function ‪_validateRoute($route)
684  {
685  // Split on comma.
686  $domains = explode(',', trim($route));
687  foreach ($domains as $domain) {
688  $domain = str_replace('@', '', trim($domain));
689  if (!$this->‪_validateDomain($domain)) {
690  return false;
691  }
692  }
693  return $route;
694  }
695 
706  protected function ‪_validateDomain($domain)
707  {
708  $sub_domains = [];
709  // Note the different use of $subdomains and $sub_domains
710  $subdomains = explode('.', $domain);
711  while (!empty($subdomains)) {
712  $sub_domains[] = $this->‪_splitCheck($subdomains, '.');
713  for ($i = 0; $i < $this->index + 1; $i++) {
714  array_shift($subdomains);
715  }
716  }
717  foreach ($sub_domains as $sub_domain) {
718  if (!$this->‪_validateSubdomain(trim($sub_domain))) {
719  return false;
720  }
721  }
722  // Managed to get here, so return input.
723  return $domain;
724  }
725 
734  protected function ‪_validateSubdomain($subdomain)
735  {
736  if (preg_match('|^\\[(.*)]$|', $subdomain, $arr)) {
737  if (!$this->‪_validateDliteral($arr[1])) {
738  return false;
739  }
740  } else {
741  if (!$this->‪_validateAtom($subdomain)) {
742  return false;
743  }
744  }
745  // Got here, so return successful.
746  return true;
747  }
748 
757  protected function ‪_validateDliteral($dliteral)
758  {
759  return !preg_match('/(.)[][\\x0D\\\\]/', $dliteral, $matches) && $matches[1] !== '\\';
760  }
761 
771  protected function ‪_validateAddrSpec($addr_spec)
772  {
773  $addr_spec = trim($addr_spec);
774  // Split on @ sign if there is one.
775  if (str_contains($addr_spec, '@')) {
776  $parts = explode('@', $addr_spec);
777  $local_part = $this->‪_splitCheck($parts, '@');
778  $domain = substr($addr_spec, strlen($local_part . '@'));
779  } else {
780  $local_part = $addr_spec;
781  $domain = ‪$this->default_domain;
782  }
783  if (($local_part = $this->‪_validateLocalPart($local_part)) === false) {
784  return false;
785  }
786  if (($domain = $this->‪_validateDomain($domain)) === false) {
787  return false;
788  }
789  // Got here so return successful.
790  return ['local_part' => $local_part, 'domain' => $domain];
791  }
792 
801  protected function ‪_validateLocalPart($local_part)
802  {
803  $parts = explode('.', $local_part);
804  $words = [];
805  // Split the local_part into words.
806  while (!empty($parts)) {
807  $words[] = $this->‪_splitCheck($parts, '.');
808  for ($i = 0; $i < $this->index + 1; $i++) {
809  array_shift($parts);
810  }
811  }
812  // Validate each word.
813  foreach ($words as $word) {
814  // If this word contains an unquoted space, it is invalid. (6.2.4)
815  if (strpos($word, ' ') && $word[0] !== '"') {
816  return false;
817  }
818  if ($this->‪_validatePhrase(trim($word)) === false) {
819  return false;
820  }
821  }
822  // Managed to get here, so return the input.
823  return $local_part;
824  }
825 }
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser
Definition: Rfc822AddressesParser.php:67
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$num_groups
‪int $num_groups
Definition: Rfc822AddressesParser.php:114
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateAddrSpec
‪mixed _validateAddrSpec($addr_spec)
Definition: Rfc822AddressesParser.php:762
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateRoute
‪string bool _validateRoute($route)
Definition: Rfc822AddressesParser.php:674
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateRouteAddr
‪mixed _validateRouteAddr($route_addr)
Definition: Rfc822AddressesParser.php:632
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateLocalPart
‪mixed _validateLocalPart($local_part)
Definition: Rfc822AddressesParser.php:792
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateDliteral
‪bool _validateDliteral($dliteral)
Definition: Rfc822AddressesParser.php:748
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$index
‪int null $index
Definition: Rfc822AddressesParser.php:108
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_splitCheck
‪mixed _splitCheck($parts, $char)
Definition: Rfc822AddressesParser.php:298
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$limit
‪int $limit
Definition: Rfc822AddressesParser.php:120
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_hasUnclosedBrackets
‪bool _hasUnclosedBrackets($string, $chars)
Definition: Rfc822AddressesParser.php:357
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$default_domain
‪string $default_domain
Definition: Rfc822AddressesParser.php:78
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$structure
‪array $structure
Definition: Rfc822AddressesParser.php:96
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\parseAddressList
‪array parseAddressList($address=null, $default_domain=null, $validate=null, $limit=null)
Definition: Rfc822AddressesParser.php:156
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$validate
‪bool $validate
Definition: Rfc822AddressesParser.php:84
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_hasUnclosedQuotes
‪bool _hasUnclosedQuotes($string)
Definition: Rfc822AddressesParser.php:325
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_isGroup
‪bool _isGroup($address)
Definition: Rfc822AddressesParser.php:272
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\validateMailbox
‪bool validateMailbox(&$mailbox)
Definition: Rfc822AddressesParser.php:556
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$addresses
‪array $addresses
Definition: Rfc822AddressesParser.php:90
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_splitAddresses
‪string false _splitAddresses($address)
Definition: Rfc822AddressesParser.php:202
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_hasUnclosedBracketsSub
‪int _hasUnclosedBracketsSub($string, &$num, $char)
Definition: Rfc822AddressesParser.php:379
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\__construct
‪__construct($address=null, $default_domain=null, $validate=null, $limit=null)
Definition: Rfc822AddressesParser.php:130
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validatePhrase
‪bool _validatePhrase($phrase)
Definition: Rfc822AddressesParser.php:471
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateDomain
‪mixed _validateDomain($domain)
Definition: Rfc822AddressesParser.php:697
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$error
‪string null $error
Definition: Rfc822AddressesParser.php:102
‪TYPO3\CMS\Core\Mail
Definition: DelayedTransportInterface.php:18
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateAddress
‪mixed _validateAddress($address)
Definition: Rfc822AddressesParser.php:404
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateQuotedString
‪bool _validateQuotedString($qstring)
Definition: Rfc822AddressesParser.php:540
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateAtom
‪bool _validateAtom($atom)
Definition: Rfc822AddressesParser.php:511
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateSubdomain
‪bool _validateSubdomain($subdomain)
Definition: Rfc822AddressesParser.php:725
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$address
‪string false $address
Definition: Rfc822AddressesParser.php:72