TYPO3 CMS  TYPO3_6-2
Rfc822AddressesParser.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Core\Mail;
3 
71 
77  private $address = '';
78 
84  private $default_domain = 'localhost';
85 
91  private $validate = TRUE;
92 
98  private $addresses = array();
99 
105  private $structure = array();
106 
112  private $error = NULL;
113 
119  private $index = NULL;
120 
127  private $num_groups = 0;
128 
134  private $limit = NULL;
135 
144  public function __construct($address = NULL, $default_domain = NULL, $validate = NULL, $limit = NULL) {
145  if (isset($address)) {
146  $this->address = $address;
147  }
148  if (isset($default_domain)) {
149  $this->default_domain = $default_domain;
150  }
151  if (isset($validate)) {
152  $this->validate = $validate;
153  }
154  if (isset($limit)) {
155  $this->limit = $limit;
156  }
157  }
158 
170  public function parseAddressList($address = NULL, $default_domain = NULL, $validate = NULL, $limit = NULL) {
171  if (isset($address)) {
172  $this->address = $address;
173  }
174  if (isset($default_domain)) {
175  $this->default_domain = $default_domain;
176  }
177  if (isset($validate)) {
178  $this->validate = $validate;
179  }
180  if (isset($limit)) {
181  $this->limit = $limit;
182  }
183  $this->structure = array();
184  $this->addresses = array();
185  $this->error = NULL;
186  $this->index = NULL;
187  // Unfold any long lines in $this->address.
188  $this->address = preg_replace('/\\r?\\n/', '
189 ', $this->address);
190  $this->address = preg_replace('/\\r\\n(\\t| )+/', ' ', $this->address);
191  while ($this->address = $this->_splitAddresses($this->address)) {
192 
193  }
194  if ($this->address === FALSE || isset($this->error)) {
195  throw new \InvalidArgumentException($this->error, 1294681466);
196  }
197  // Validate each address individually. If we encounter an invalid
198  // address, stop iterating and return an error immediately.
199  foreach ($this->addresses as $address) {
200  $valid = $this->_validateAddress($address);
201  if ($valid === FALSE || isset($this->error)) {
202  throw new \InvalidArgumentException($this->error, 1294681467);
203  }
204  $this->structure = array_merge($this->structure, $valid);
205  }
206  return $this->structure;
207  }
208 
216  protected function _splitAddresses($address) {
217  if (!empty($this->limit) && count($this->addresses) == $this->limit) {
218  return '';
219  }
220  if ($this->_isGroup($address) && !isset($this->error)) {
221  $split_char = ';';
222  $is_group = TRUE;
223  } elseif (!isset($this->error)) {
224  $split_char = ',';
225  $is_group = FALSE;
226  } elseif (isset($this->error)) {
227  return FALSE;
228  }
229  // Split the string based on the above ten or so lines.
230  $parts = explode($split_char, $address);
231  $string = $this->_splitCheck($parts, $split_char);
232  // If a group...
233  if ($is_group) {
234  // If $string does not contain a colon outside of
235  // brackets/quotes etc then something's fubar.
236  // First check there's a colon at all:
237  if (strpos($string, ':') === FALSE) {
238  $this->error = 'Invalid address: ' . $string;
239  return FALSE;
240  }
241  // Now check it's outside of brackets/quotes:
242  if (!$this->_splitCheck(explode(':', $string), ':')) {
243  return FALSE;
244  }
245  // We must have a group at this point, so increase the counter:
246  $this->num_groups++;
247  }
248  // $string now contains the first full address/group.
249  // Add to the addresses array.
250  $this->addresses[] = array(
251  'address' => trim($string),
252  'group' => $is_group
253  );
254  // Remove the now stored address from the initial line, the +1
255  // is to account for the explode character.
256  $address = trim(substr($address, strlen($string) + 1));
257  // If the next char is a comma and this was a group, then
258  // there are more addresses, otherwise, if there are any more
259  // chars, then there is another address.
260  if ($is_group && $address[0] === ',') {
261  $address = trim(substr($address, 1));
262  return $address;
263  } elseif (strlen($address) > 0) {
264  return $address;
265  } else {
266  return '';
267  }
268  }
269 
277  protected function _isGroup($address) {
278  // First comma not in quotes, angles or escaped:
279  $parts = explode(',', $address);
280  $string = $this->_splitCheck($parts, ',');
281  // Now we have the first address, we can reliably check for a
282  // group by searching for a colon that's not escaped or in
283  // quotes or angle brackets.
284  if (count(($parts = explode(':', $string))) > 1) {
285  $string2 = $this->_splitCheck($parts, ':');
286  return $string2 !== $string;
287  } else {
288  return FALSE;
289  }
290  }
291 
300  protected function _splitCheck($parts, $char) {
301  $string = $parts[0];
302  $partsCounter = count($parts);
303  for ($i = 0; $i < $partsCounter; $i++) {
304  if ($this->_hasUnclosedQuotes($string) || $this->_hasUnclosedBrackets($string, '<>') || $this->_hasUnclosedBrackets($string, '[]') || $this->_hasUnclosedBrackets($string, '()') || substr($string, -1) == '\\') {
305  if (isset($parts[$i + 1])) {
306  $string = $string . $char . $parts[($i + 1)];
307  } else {
308  $this->error = 'Invalid address spec. Unclosed bracket or quotes';
309  return FALSE;
310  }
311  } else {
312  $this->index = $i;
313  break;
314  }
315  }
316  return $string;
317  }
318 
326  protected function _hasUnclosedQuotes($string) {
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  $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  } else {
365  return $num_angle_start > $num_angle_end;
366  }
367  }
368 
378  protected function _hasUnclosedBracketsSub($string, &$num, $char) {
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  $is_group = FALSE;
401  $addresses = array();
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 = array();
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 (strlen($address['address']) > 0) {
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 (!count($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  // Splits on one or more Tab or space.
466  $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
467  $phrase_parts = array();
468  while (count($parts) > 0) {
469  $phrase_parts[] = $this->_splitCheck($parts, ' ');
470  for ($i = 0; $i < $this->index + 1; $i++) {
471  array_shift($parts);
472  }
473  }
474  foreach ($phrase_parts as $part) {
475  // If quoted string:
476  if ($part[0] === '"') {
477  if (!$this->_validateQuotedString($part)) {
478  return FALSE;
479  }
480  continue;
481  }
482  // Otherwise it's an atom:
483  if (!$this->_validateAtom($part)) {
484  return FALSE;
485  }
486  }
487  return TRUE;
488  }
489 
503  protected function _validateAtom($atom) {
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  // Leading and trailing "
533  $qstring = substr($qstring, 1, -1);
534  // Perform check, removing quoted characters first.
535  return !preg_match('/[\\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
536  }
537 
547  protected function validateMailbox(&$mailbox) {
548  // A couple of defaults.
549  $phrase = '';
550  $comment = '';
551  $comments = array();
552  // Catch any RFC822 comments and store them separately.
553  $_mailbox = $mailbox;
554  while (strlen(trim($_mailbox)) > 0) {
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 = isset($comments) ? $comments : array();
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  // Check for colon.
623  if (strpos($route_addr, ':') !== FALSE) {
624  $parts = explode(':', $route_addr);
625  $route = $this->_splitCheck($parts, ':');
626  } else {
627  $route = $route_addr;
628  }
629  // If $route is same as $route_addr then the colon was in
630  // quotes or brackets or, of course, non existent.
631  if ($route === $route_addr) {
632  unset($route);
633  $addr_spec = $route_addr;
634  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === FALSE) {
635  return FALSE;
636  }
637  } else {
638  // Validate route part.
639  if (($route = $this->_validateRoute($route)) === FALSE) {
640  return FALSE;
641  }
642  $addr_spec = substr($route_addr, strlen($route . ':'));
643  // Validate addr-spec part.
644  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === FALSE) {
645  return FALSE;
646  }
647  }
648  if (isset($route)) {
649  $return['adl'] = $route;
650  } else {
651  $return['adl'] = '';
652  }
653  $return = array_merge($return, $addr_spec);
654  return $return;
655  }
656 
665  protected function _validateRoute($route) {
666  // Split on comma.
667  $domains = explode(',', trim($route));
668  foreach ($domains as $domain) {
669  $domain = str_replace('@', '', trim($domain));
670  if (!$this->_validateDomain($domain)) {
671  return FALSE;
672  }
673  }
674  return $route;
675  }
676 
687  protected function _validateDomain($domain) {
688  // Note the different use of $subdomains and $sub_domains
689  $subdomains = explode('.', $domain);
690  while (count($subdomains) > 0) {
691  $sub_domains[] = $this->_splitCheck($subdomains, '.');
692  for ($i = 0; $i < $this->index + 1; $i++) {
693  array_shift($subdomains);
694  }
695  }
696  foreach ($sub_domains as $sub_domain) {
697  if (!$this->_validateSubdomain(trim($sub_domain))) {
698  return FALSE;
699  }
700  }
701  // Managed to get here, so return input.
702  return $domain;
703  }
704 
713  protected function _validateSubdomain($subdomain) {
714  if (preg_match('|^\\[(.*)]$|', $subdomain, $arr)) {
715  if (!$this->_validateDliteral($arr[1])) {
716  return FALSE;
717  }
718  } else {
719  if (!$this->_validateAtom($subdomain)) {
720  return FALSE;
721  }
722  }
723  // Got here, so return successful.
724  return TRUE;
725  }
726 
735  protected function _validateDliteral($dliteral) {
736  return !preg_match('/(.)[][\\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\';
737  }
738 
748  protected function _validateAddrSpec($addr_spec) {
749  $addr_spec = trim($addr_spec);
750  // Split on @ sign if there is one.
751  if (strpos($addr_spec, '@') !== FALSE) {
752  $parts = explode('@', $addr_spec);
753  $local_part = $this->_splitCheck($parts, '@');
754  $domain = substr($addr_spec, strlen($local_part . '@'));
755  } else {
756  $local_part = $addr_spec;
757  $domain = $this->default_domain;
758  }
759  if (($local_part = $this->_validateLocalPart($local_part)) === FALSE) {
760  return FALSE;
761  }
762  if (($domain = $this->_validateDomain($domain)) === FALSE) {
763  return FALSE;
764  }
765  // Got here so return successful.
766  return array('local_part' => $local_part, 'domain' => $domain);
767  }
768 
777  protected function _validateLocalPart($local_part) {
778  $parts = explode('.', $local_part);
779  $words = array();
780  // Split the local_part into words.
781  while (count($parts) > 0) {
782  $words[] = $this->_splitCheck($parts, '.');
783  for ($i = 0; $i < $this->index + 1; $i++) {
784  array_shift($parts);
785  }
786  }
787  // Validate each word.
788  foreach ($words as $word) {
789  // If this word contains an unquoted space, it is invalid. (6.2.4)
790  if (strpos($word, ' ') && $word[0] !== '"') {
791  return FALSE;
792  }
793  if ($this->_validatePhrase(trim($word)) === FALSE) {
794  return FALSE;
795  }
796  }
797  // Managed to get here, so return the input.
798  return $local_part;
799  }
800 
801 }
__construct($address=NULL, $default_domain=NULL, $validate=NULL, $limit=NULL)
parseAddressList($address=NULL, $default_domain=NULL, $validate=NULL, $limit=NULL)