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