TYPO3 CMS  TYPO3_7-6
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  } elseif ($address !== '') {
259  return $address;
260  } else {
261  return '';
262  }
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  } else {
284  return false;
285  }
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  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  } else {
364  return $num_angle_start > $num_angle_end;
365  }
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  $is_group = false;
402  $addresses = [];
403  if ($address['group']) {
404  $is_group = true;
405  // Get the group part of the name
406  $parts = explode(':', $address['address']);
407  $groupname = $this->_splitCheck($parts, ':');
408  $structure = [];
409  // And validate the group part of the name.
410  if (!$this->_validatePhrase($groupname)) {
411  $this->error = 'Group name did not validate.';
412  return false;
413  }
414  $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
415  }
416  // If a group then split on comma and put into an array.
417  // Otherwise, Just put the whole address in an array.
418  if ($is_group) {
419  while ($address['address'] !== '') {
420  $parts = explode(',', $address['address']);
421  $addresses[] = $this->_splitCheck($parts, ',');
422  $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
423  }
424  } else {
425  $addresses[] = $address['address'];
426  }
427  // Check that $addresses is set, if address like this:
428  // Groupname:;
429  // Then errors were appearing.
430  if (empty($addresses)) {
431  $this->error = 'Empty group.';
432  return false;
433  }
434  // Trim the whitespace from all of the address strings.
435  array_map('trim', $addresses);
436  // Validate each mailbox.
437  // Format could be one of: name <geezer@domain.com>
438  // geezer@domain.com
439  // geezer
440  // ... or any other format valid by RFC 822.
441  $addressesCount = count($addresses);
442  for ($i = 0; $i < $addressesCount; $i++) {
443  if (!$this->validateMailbox($addresses[$i])) {
444  if (empty($this->error)) {
445  $this->error = 'Validation failed for: ' . $addresses[$i];
446  }
447  return false;
448  }
449  }
450  if ($is_group) {
451  $structure = array_merge($structure, $addresses);
452  } else {
454  }
455  return $structure;
456  }
457 
465  protected function _validatePhrase($phrase)
466  {
467  // Splits on one or more Tab or space.
468  $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
469  $phrase_parts = [];
470  while (!empty($parts)) {
471  $phrase_parts[] = $this->_splitCheck($parts, ' ');
472  for ($i = 0; $i < $this->index + 1; $i++) {
473  array_shift($parts);
474  }
475  }
476  foreach ($phrase_parts as $part) {
477  // If quoted string:
478  if ($part[0] === '"') {
479  if (!$this->_validateQuotedString($part)) {
480  return false;
481  }
482  continue;
483  }
484  // Otherwise it's an atom:
485  if (!$this->_validateAtom($part)) {
486  return false;
487  }
488  }
489  return true;
490  }
491 
505  protected function _validateAtom($atom)
506  {
507  if (!$this->validate) {
508  // Validation has been turned off; assume the atom is okay.
509  return true;
510  }
511  // Check for any char from ASCII 0 - ASCII 127
512  if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
513  return false;
514  }
515  // Check for specials:
516  if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
517  return false;
518  }
519  // Check for control characters (ASCII 0-31):
520  if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
521  return false;
522  }
523  return true;
524  }
525 
534  protected function _validateQuotedString($qstring)
535  {
536  // Leading and trailing "
537  $qstring = substr($qstring, 1, -1);
538  // Perform check, removing quoted characters first.
539  return !preg_match('/[\\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
540  }
541 
551  protected function validateMailbox(&$mailbox)
552  {
553  // A couple of defaults.
554  $phrase = '';
555  $comment = '';
556  $comments = [];
557  // Catch any RFC822 comments and store them separately.
558  $_mailbox = $mailbox;
559  while (trim($_mailbox) !== '') {
560  $parts = explode('(', $_mailbox);
561  $before_comment = $this->_splitCheck($parts, '(');
562  if ($before_comment != $_mailbox) {
563  // First char should be a (.
564  $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
565  $parts = explode(')', $comment);
566  $comment = $this->_splitCheck($parts, ')');
567  $comments[] = $comment;
568  // +2 is for the brackets
569  $_mailbox = substr($_mailbox, strpos($_mailbox, ('(' . $comment)) + strlen($comment) + 2);
570  } else {
571  break;
572  }
573  }
574  foreach ($comments as $comment) {
575  $mailbox = str_replace('(' . $comment . ')', '', $mailbox);
576  }
577  $mailbox = trim($mailbox);
578  // Check for name + route-addr
579  if (substr($mailbox, -1) === '>' && $mailbox[0] !== '<') {
580  $parts = explode('<', $mailbox);
581  $name = $this->_splitCheck($parts, '<');
582  $phrase = trim($name);
583  $route_addr = trim(substr($mailbox, strlen($name . '<'), -1));
584  if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {
585  return false;
586  }
587  } else {
588  // First snip angle brackets if present.
589  if ($mailbox[0] === '<' && substr($mailbox, -1) === '>') {
590  $addr_spec = substr($mailbox, 1, -1);
591  } else {
592  $addr_spec = $mailbox;
593  }
594  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
595  return false;
596  }
597  }
598  // Construct the object that will be returned.
599  $mbox = new \stdClass();
600  // Add the phrase (even if empty) and comments
601  $mbox->personal = $phrase;
602  $mbox->comment = isset($comments) ? $comments : [];
603  if (isset($route_addr)) {
604  $mbox->mailbox = $route_addr['local_part'];
605  $mbox->host = $route_addr['domain'];
606  $route_addr['adl'] !== '' ? ($mbox->adl = $route_addr['adl']) : '';
607  } else {
608  $mbox->mailbox = $addr_spec['local_part'];
609  $mbox->host = $addr_spec['domain'];
610  }
611  $mailbox = $mbox;
612  return true;
613  }
614 
626  protected function _validateRouteAddr($route_addr)
627  {
628  // Check for colon.
629  if (strpos($route_addr, ':') !== false) {
630  $parts = explode(':', $route_addr);
631  $route = $this->_splitCheck($parts, ':');
632  } else {
633  $route = $route_addr;
634  }
635  // If $route is same as $route_addr then the colon was in
636  // quotes or brackets or, of course, non existent.
637  if ($route === $route_addr) {
638  unset($route);
639  $addr_spec = $route_addr;
640  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
641  return false;
642  }
643  } else {
644  // Validate route part.
645  if (($route = $this->_validateRoute($route)) === false) {
646  return false;
647  }
648  $addr_spec = substr($route_addr, strlen($route . ':'));
649  // Validate addr-spec part.
650  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
651  return false;
652  }
653  }
654  if (isset($route)) {
655  $return['adl'] = $route;
656  } else {
657  $return['adl'] = '';
658  }
659  $return = array_merge($return, $addr_spec);
660  return $return;
661  }
662 
671  protected function _validateRoute($route)
672  {
673  // Split on comma.
674  $domains = explode(',', trim($route));
675  foreach ($domains as $domain) {
676  $domain = str_replace('@', '', trim($domain));
677  if (!$this->_validateDomain($domain)) {
678  return false;
679  }
680  }
681  return $route;
682  }
683 
694  protected function _validateDomain($domain)
695  {
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 }
__construct($address=null, $default_domain=null, $validate=null, $limit=null)
parseAddressList($address=null, $default_domain=null, $validate=null, $limit=null)