‪TYPO3CMS  10.4
Rfc822AddressesParser.php
Go to the documentation of this file.
1 <?php
2 
3 namespace ‪TYPO3\CMS\Core\Mail;
4 
65 {
71  private ‪$address = '';
72 
78  private ‪$default_domain = 'localhost';
79 
85  private ‪$validate = true;
86 
92  private ‪$addresses = [];
93 
99  private ‪$structure = [];
100 
106  private ‪$error;
107 
113  private ‪$index;
114 
120  private ‪$num_groups = 0;
121 
127  private ‪$limit;
128 
137  public function ‪__construct(‪$address = null, ‪$default_domain = null, ‪$validate = null, ‪$limit = null)
138  {
139  if (isset(‪$address)) {
140  $this->address = ‪$address;
141  }
142  if (isset(‪$default_domain)) {
143  $this->default_domain = ‪$default_domain;
144  }
145  if (isset(‪$validate)) {
146  $this->validate = ‪$validate;
147  }
148  if (isset(‪$limit)) {
149  $this->limit = ‪$limit;
150  }
151  }
152 
163  public function ‪parseAddressList(‪$address = null, ‪$default_domain = null, ‪$validate = null, ‪$limit = null)
164  {
165  if (isset(‪$address)) {
166  $this->address = ‪$address;
167  }
168  if (isset(‪$default_domain)) {
169  $this->default_domain = ‪$default_domain;
170  }
171  if (isset(‪$validate)) {
172  $this->validate = ‪$validate;
173  }
174  if (isset(‪$limit)) {
175  $this->limit = ‪$limit;
176  }
177  $this->structure = [];
178  $this->addresses = [];
179  $this->error = null;
180  $this->index = null;
181  // Unfold any long lines in $this->address.
182  $this->address = (string)preg_replace('/\\r?\\n/', '
183 ', $this->address);
184  $this->address = (string)preg_replace('/\\r\\n(\\t| )+/', ' ', $this->address);
185  while ($this->address = $this->‪_splitAddresses($this->address)) {
186  }
187  if ($this->address === false || isset($this->error)) {
188  throw new \InvalidArgumentException((string)$this->error, 1294681466);
189  }
190  // Validate each address individually. If we encounter an invalid
191  // address, stop iterating and return an error immediately.
192  foreach ($this->addresses as ‪$address) {
193  $valid = $this->‪_validateAddress($address);
194  if ($valid === false || isset($this->error)) {
195  throw new \InvalidArgumentException((string)$this->error, 1294681467);
196  }
197  $this->structure = array_merge($this->structure, $valid);
198  }
199  return ‪$this->structure;
200  }
201 
209  protected function ‪_splitAddresses(‪$address)
210  {
211  $split_char = '';
212  $is_group = false;
213  if (!empty($this->limit) && count($this->addresses) == $this->limit) {
214  return '';
215  }
216  if ($this->‪_isGroup($address) && !isset($this->error)) {
217  $split_char = ';';
218  $is_group = true;
219  } elseif (!isset($this->error)) {
220  $split_char = ',';
221  $is_group = false;
222  } elseif (isset($this->error)) {
223  return false;
224  }
225  // Split the string based on the above ten or so lines.
226  $parts = explode($split_char, ‪$address) ?: [];
227  $string = $this->‪_splitCheck($parts, $split_char);
228  // If a group...
229  if ($is_group) {
230  // If $string does not contain a colon outside of
231  // brackets/quotes etc then something's fubar.
232  // First check there's a colon at all:
233  if (strpos($string, ':') === false) {
234  $this->error = 'Invalid address: ' . $string;
235  return false;
236  }
237  // Now check it's outside of brackets/quotes:
238  if (!$this->‪_splitCheck(explode(':', $string), ':')) {
239  return false;
240  }
241  // We must have a group at this point, so increase the counter:
242  $this->num_groups++;
243  }
244  // $string now contains the first full address/group.
245  // Add to the addresses array.
246  $this->addresses[] = [
247  'address' => trim($string),
248  'group' => $is_group
249  ];
250  // Remove the now stored address from the initial line, the +1
251  // is to account for the explode character.
252  ‪$address = trim(substr(‪$address, strlen($string) + 1));
253  // If the next char is a comma and this was a group, then
254  // there are more addresses, otherwise, if there are any more
255  // chars, then there is another address.
256  if ($is_group && ‪$address[0] === ',') {
257  ‪$address = trim(substr(‪$address, 1));
258  return ‪$address;
259  }
260  if (‪$address !== '') {
261  return ‪$address;
262  }
263  return '';
264  }
265 
273  protected function ‪_isGroup(‪$address)
274  {
275  // First comma not in quotes, angles or escaped:
276  $parts = explode(',', ‪$address);
277  $string = $this->‪_splitCheck($parts, ',');
278  // Now we have the first address, we can reliably check for a
279  // group by searching for a colon that's not escaped or in
280  // quotes or angle brackets.
281  if (count($parts = explode(':', $string)) > 1) {
282  $string2 = $this->‪_splitCheck($parts, ':');
283  return $string2 !== $string;
284  }
285  return false;
286  }
287 
296  protected function ‪_splitCheck($parts, $char)
297  {
298  $string = $parts[0];
299  $partsCounter = count($parts);
300  for ($i = 0; $i < $partsCounter; $i++) {
301  if ($this->‪_hasUnclosedQuotes($string) || $this->‪_hasUnclosedBrackets($string, '<>') || $this->‪_hasUnclosedBrackets($string, '[]') || $this->‪_hasUnclosedBrackets($string, '()') || substr($string, -1) === '\\') {
302  if (isset($parts[$i + 1])) {
303  $string = $string . $char . $parts[$i + 1];
304  } else {
305  $this->error = 'Invalid address spec. Unclosed bracket or quotes';
306  return false;
307  }
308  } else {
309  $this->index = $i;
310  break;
311  }
312  }
313  return $string;
314  }
315 
323  protected function ‪_hasUnclosedQuotes($string)
324  {
325  $string = trim($string);
326  $iMax = strlen($string);
327  $in_quote = false;
328  $i = ($slashes = 0);
329  for (; $i < $iMax; ++$i) {
330  switch ($string[$i]) {
331  case '\\':
332  ++$slashes;
333  break;
334  case '"':
335  if ($slashes % 2 == 0) {
336  $in_quote = !$in_quote;
337  }
338  // no break
339  default:
340  $slashes = 0;
341  }
342  }
343  return $in_quote;
344  }
345 
355  protected function ‪_hasUnclosedBrackets($string, $chars)
356  {
357  $num_angle_start = substr_count($string, $chars[0]);
358  $num_angle_end = substr_count($string, $chars[1]);
359  $this->‪_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
360  $this->‪_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
361  if ($num_angle_start < $num_angle_end) {
362  $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
363  return false;
364  }
365  return $num_angle_start > $num_angle_end;
366  }
367 
377  protected function ‪_hasUnclosedBracketsSub($string, &$num, $char)
378  {
379  $parts = explode($char, $string) ?: [];
380  $partsCounter = count($parts);
381  for ($i = 0; $i < $partsCounter; $i++) {
382  if (substr($parts[$i], -1) === '\\' || $this->‪_hasUnclosedQuotes($parts[$i])) {
383  $num--;
384  }
385  if (isset($parts[$i + 1])) {
386  $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
387  }
388  }
389  return $num;
390  }
391 
399  protected function ‪_validateAddress(‪$address)
400  {
401  ‪$structure = [];
402  $is_group = false;
403  ‪$addresses = [];
404  if (‪$address['group']) {
405  $is_group = true;
406  // Get the group part of the name
407  $parts = explode(':', ‪$address['address']);
408  $groupname = $this->‪_splitCheck($parts, ':');
409  ‪$structure = [];
410  // And validate the group part of the name.
411  if (!$this->‪_validatePhrase($groupname)) {
412  $this->error = 'Group name did not validate.';
413  return false;
414  }
415  ‪$address['address'] = ltrim(substr(‪$address['address'], strlen($groupname . ':')));
416  }
417  // If a group then split on comma and put into an array.
418  // Otherwise, Just put the whole address in an array.
419  if ($is_group) {
420  while (‪$address['address'] !== '') {
421  $parts = explode(',', ‪$address['address']);
422  ‪$addresses[] = $this->‪_splitCheck($parts, ',');
423  ‪$address['address'] = trim(substr(‪$address['address'], strlen(end(‪$addresses) . ',')));
424  }
425  } else {
426  ‪$addresses[] = ‪$address['address'];
427  }
428  // Check that $addresses is set, if address like this:
429  // Groupname:;
430  // Then errors were appearing.
431  if (empty(‪$addresses)) {
432  $this->error = 'Empty group.';
433  return false;
434  }
435  // Trim the whitespace from all of the address strings.
436  array_map('trim', ‪$addresses);
437  // Validate each mailbox.
438  // Format could be one of: name <geezer@domain.com>
439  // geezer@domain.com
440  // geezer
441  // ... or any other format valid by RFC 822.
442  $addressesCount = count(‪$addresses);
443  for ($i = 0; $i < $addressesCount; $i++) {
444  if (!$this->‪validateMailbox(‪$addresses[$i])) {
445  if (empty($this->error)) {
446  $this->error = 'Validation failed for: ' . ‪$addresses[$i];
447  }
448  return false;
449  }
450  }
451  if ($is_group) {
452  ‪$structure = array_merge(‪$structure, ‪$addresses);
453  } else {
455  }
456  return ‪$structure;
457  }
458 
466  protected function ‪_validatePhrase($phrase)
467  {
468  // Splits on one or more Tab or space.
469  $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
470  $phrase_parts = [];
471  while (!empty($parts)) {
472  $phrase_parts[] = $this->‪_splitCheck($parts, ' ');
473  for ($i = 0; $i < $this->index + 1; $i++) {
474  array_shift($parts);
475  }
476  }
477  foreach ($phrase_parts as $part) {
478  // If quoted string:
479  if ($part[0] === '"') {
480  if (!$this->‪_validateQuotedString($part)) {
481  return false;
482  }
483  continue;
484  }
485  // Otherwise it's an atom:
486  if (!$this->‪_validateAtom($part)) {
487  return false;
488  }
489  }
490  return true;
491  }
492 
506  protected function ‪_validateAtom($atom)
507  {
508  if (!$this->validate) {
509  // Validation has been turned off; assume the atom is okay.
510  return true;
511  }
512  // Check for any char from ASCII 0 - ASCII 127
513  if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
514  return false;
515  }
516  // Check for specials:
517  if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
518  return false;
519  }
520  // Check for control characters (ASCII 0-31):
521  if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
522  return false;
523  }
524  return true;
525  }
526 
535  protected function ‪_validateQuotedString($qstring)
536  {
537  // Leading and trailing "
538  $qstring = (string)substr($qstring, 1, -1);
539  // Perform check, removing quoted characters first.
540  return !preg_match('/[\\x0D\\\\"]/', (string)preg_replace('/\\\\./', '', $qstring));
541  }
542 
551  protected function ‪validateMailbox(&$mailbox)
552  {
553  $route_addr = null;
554  $addr_spec = [];
555  // A couple of defaults.
556  $phrase = '';
557  $comments = [];
558  // Catch any RFC822 comments and store them separately.
559  $_mailbox = $mailbox;
560  while (trim($_mailbox) !== '') {
561  $parts = explode('(', $_mailbox);
562  $before_comment = $this->‪_splitCheck($parts, '(');
563  if ($before_comment != $_mailbox) {
564  // First char should be a (.
565  $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
566  $parts = explode(')', $comment);
567  $comment = $this->‪_splitCheck($parts, ')');
568  $comments[] = $comment;
569  // +2 is for the brackets
570  $_mailbox = substr($_mailbox, strpos($_mailbox, '(' . $comment) + strlen($comment) + 2);
571  } else {
572  break;
573  }
574  }
575  foreach ($comments as $comment) {
576  $mailbox = str_replace('(' . $comment . ')', '', $mailbox);
577  }
578  $mailbox = trim($mailbox);
579  // Check for name + route-addr
580  if (substr($mailbox, -1) === '>' && $mailbox[0] !== '<') {
581  $parts = explode('<', $mailbox);
582  $name = $this->‪_splitCheck($parts, '<');
583  $phrase = trim($name);
584  $route_addr = trim(substr($mailbox, strlen($name . '<'), -1));
585  if ($this->‪_validatePhrase($phrase) === false || ($route_addr = $this->‪_validateRouteAddr($route_addr)) === false) {
586  return false;
587  }
588  } else {
589  // First snip angle brackets if present.
590  if ($mailbox[0] === '<' && substr($mailbox, -1) === '>') {
591  $addr_spec = substr($mailbox, 1, -1);
592  } else {
593  $addr_spec = $mailbox;
594  }
595  if (($addr_spec = $this->‪_validateAddrSpec($addr_spec)) === false) {
596  return false;
597  }
598  }
599  // Construct the object that will be returned.
600  $mbox = new \stdClass();
601  // Add the phrase (even if empty) and comments
602  $mbox->personal = $phrase;
603  $mbox->comment = $comments ?? [];
604  if (isset($route_addr)) {
605  $mbox->mailbox = $route_addr['local_part'];
606  $mbox->host = $route_addr['domain'];
607  $route_addr['adl'] !== '' ? ($mbox->adl = $route_addr['adl']) : '';
608  } else {
609  $mbox->mailbox = $addr_spec['local_part'];
610  $mbox->host = $addr_spec['domain'];
611  }
612  $mailbox = $mbox;
613  return true;
614  }
615 
627  protected function ‪_validateRouteAddr($route_addr)
628  {
629  $route = '';
630  $return = [];
631  // Check for colon.
632  if (strpos($route_addr, ':') !== false) {
633  $parts = explode(':', $route_addr);
634  $route = $this->‪_splitCheck($parts, ':');
635  } else {
636  $route = $route_addr;
637  }
638  // If $route is same as $route_addr then the colon was in
639  // quotes or brackets or, of course, non existent.
640  if ($route === $route_addr) {
641  $route = '';
642  $addr_spec = $route_addr;
643  if (($addr_spec = $this->‪_validateAddrSpec($addr_spec)) === false) {
644  return false;
645  }
646  } else {
647  // Validate route part.
648  if (($route = $this->‪_validateRoute($route)) === false) {
649  return false;
650  }
651  $addr_spec = substr($route_addr, strlen($route . ':'));
652  // Validate addr-spec part.
653  if (($addr_spec = $this->‪_validateAddrSpec($addr_spec)) === false) {
654  return false;
655  }
656  }
657  $return['adl'] = $route;
658  $return = array_merge($return, $addr_spec);
659  return $return;
660  }
661 
670  protected function ‪_validateRoute($route)
671  {
672  // Split on comma.
673  $domains = explode(',', trim($route));
674  foreach ($domains as $domain) {
675  $domain = str_replace('@', '', trim($domain));
676  if (!$this->‪_validateDomain($domain)) {
677  return false;
678  }
679  }
680  return $route;
681  }
682 
693  protected function ‪_validateDomain($domain)
694  {
695  $sub_domains = [];
696  // Note the different use of $subdomains and $sub_domains
697  $subdomains = explode('.', $domain);
698  while (!empty($subdomains)) {
699  $sub_domains[] = $this->‪_splitCheck($subdomains, '.');
700  for ($i = 0; $i < $this->index + 1; $i++) {
701  array_shift($subdomains);
702  }
703  }
704  foreach ($sub_domains as $sub_domain) {
705  if (!$this->‪_validateSubdomain(trim($sub_domain))) {
706  return false;
707  }
708  }
709  // Managed to get here, so return input.
710  return $domain;
711  }
712 
721  protected function ‪_validateSubdomain($subdomain)
722  {
723  if (preg_match('|^\\[(.*)]$|', $subdomain, $arr)) {
724  if (!$this->‪_validateDliteral($arr[1])) {
725  return false;
726  }
727  } else {
728  if (!$this->‪_validateAtom($subdomain)) {
729  return false;
730  }
731  }
732  // Got here, so return successful.
733  return true;
734  }
735 
744  protected function ‪_validateDliteral($dliteral)
745  {
746  return !preg_match('/(.)[][\\x0D\\\\]/', $dliteral, $matches) && $matches[1] !== '\\';
747  }
748 
758  protected function ‪_validateAddrSpec($addr_spec)
759  {
760  $addr_spec = trim($addr_spec);
761  // Split on @ sign if there is one.
762  if (strpos($addr_spec, '@') !== false) {
763  $parts = explode('@', $addr_spec);
764  $local_part = $this->‪_splitCheck($parts, '@');
765  $domain = substr($addr_spec, strlen($local_part . '@'));
766  } else {
767  $local_part = $addr_spec;
768  $domain = ‪$this->default_domain;
769  }
770  if (($local_part = $this->‪_validateLocalPart($local_part)) === false) {
771  return false;
772  }
773  if (($domain = $this->‪_validateDomain($domain)) === false) {
774  return false;
775  }
776  // Got here so return successful.
777  return ['local_part' => $local_part, 'domain' => $domain];
778  }
779 
788  protected function ‪_validateLocalPart($local_part)
789  {
790  $parts = explode('.', $local_part);
791  $words = [];
792  // Split the local_part into words.
793  while (!empty($parts)) {
794  $words[] = $this->‪_splitCheck($parts, '.');
795  for ($i = 0; $i < $this->index + 1; $i++) {
796  array_shift($parts);
797  }
798  }
799  // Validate each word.
800  foreach ($words as $word) {
801  // If this word contains an unquoted space, it is invalid. (6.2.4)
802  if (strpos($word, ' ') && $word[0] !== '"') {
803  return false;
804  }
805  if ($this->‪_validatePhrase(trim($word)) === false) {
806  return false;
807  }
808  }
809  // Managed to get here, so return the input.
810  return $local_part;
811  }
812 }
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser
Definition: Rfc822AddressesParser.php:65
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$num_groups
‪int $num_groups
Definition: Rfc822AddressesParser.php:112
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateAddrSpec
‪mixed _validateAddrSpec($addr_spec)
Definition: Rfc822AddressesParser.php:749
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateRoute
‪string bool _validateRoute($route)
Definition: Rfc822AddressesParser.php:661
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateRouteAddr
‪mixed _validateRouteAddr($route_addr)
Definition: Rfc822AddressesParser.php:618
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateLocalPart
‪mixed _validateLocalPart($local_part)
Definition: Rfc822AddressesParser.php:779
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateDliteral
‪bool _validateDliteral($dliteral)
Definition: Rfc822AddressesParser.php:735
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_splitCheck
‪mixed _splitCheck($parts, $char)
Definition: Rfc822AddressesParser.php:287
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$limit
‪int $limit
Definition: Rfc822AddressesParser.php:118
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_hasUnclosedBrackets
‪bool _hasUnclosedBrackets($string, $chars)
Definition: Rfc822AddressesParser.php:346
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$default_domain
‪string $default_domain
Definition: Rfc822AddressesParser.php:76
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$structure
‪array $structure
Definition: Rfc822AddressesParser.php:94
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\parseAddressList
‪array parseAddressList($address=null, $default_domain=null, $validate=null, $limit=null)
Definition: Rfc822AddressesParser.php:154
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$validate
‪bool $validate
Definition: Rfc822AddressesParser.php:82
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_hasUnclosedQuotes
‪bool _hasUnclosedQuotes($string)
Definition: Rfc822AddressesParser.php:314
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_isGroup
‪bool _isGroup($address)
Definition: Rfc822AddressesParser.php:264
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\validateMailbox
‪bool validateMailbox(&$mailbox)
Definition: Rfc822AddressesParser.php:542
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$addresses
‪array $addresses
Definition: Rfc822AddressesParser.php:88
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$index
‪int $index
Definition: Rfc822AddressesParser.php:106
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_hasUnclosedBracketsSub
‪int _hasUnclosedBracketsSub($string, &$num, $char)
Definition: Rfc822AddressesParser.php:368
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\__construct
‪__construct($address=null, $default_domain=null, $validate=null, $limit=null)
Definition: Rfc822AddressesParser.php:128
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validatePhrase
‪bool _validatePhrase($phrase)
Definition: Rfc822AddressesParser.php:457
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$error
‪string $error
Definition: Rfc822AddressesParser.php:100
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateDomain
‪mixed _validateDomain($domain)
Definition: Rfc822AddressesParser.php:684
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\$address
‪string $address
Definition: Rfc822AddressesParser.php:70
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_splitAddresses
‪bool _splitAddresses($address)
Definition: Rfc822AddressesParser.php:200
‪TYPO3\CMS\Core\Mail
Definition: DelayedTransportInterface.php:18
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateAddress
‪mixed _validateAddress($address)
Definition: Rfc822AddressesParser.php:390
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateQuotedString
‪bool _validateQuotedString($qstring)
Definition: Rfc822AddressesParser.php:526
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateAtom
‪bool _validateAtom($atom)
Definition: Rfc822AddressesParser.php:497
‪TYPO3\CMS\Core\Mail\Rfc822AddressesParser\_validateSubdomain
‪bool _validateSubdomain($subdomain)
Definition: Rfc822AddressesParser.php:712