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