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