ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
RFC822.php
Go to the documentation of this file.
1 <?php
73 {
74 
79  public $address = '';
80 
85  public $default_domain = 'localhost';
86 
91  public $nestGroups = true;
92 
97  public $validate = true;
98 
103  public $addresses = array();
104 
109  public $structure = array();
110 
115  public $error = null;
116 
121  public $index = null;
122 
128  public $num_groups = 0;
129 
135  public $mailRFC822 = true;
136 
141  public $limit = null;
142 
154  public function __construct($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
155  {
156  if (isset($address)) {
157  $this->address = $address;
158  }
159  if (isset($default_domain)) {
160  $this->default_domain = $default_domain;
161  }
162  if (isset($nest_groups)) {
163  $this->nestGroups = $nest_groups;
164  }
165  if (isset($validate)) {
166  $this->validate = $validate;
167  }
168  if (isset($limit)) {
169  $this->limit = $limit;
170  }
171  }
172 
184  public function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
185  {
186  if (!isset($this) || !isset($this->mailRFC822)) {
187  $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit);
188  return $obj->parseAddressList();
189  }
190 
191  if (isset($address)) {
192  $this->address = $address;
193  }
194  if (isset($default_domain)) {
195  $this->default_domain = $default_domain;
196  }
197  if (isset($nest_groups)) {
198  $this->nestGroups = $nest_groups;
199  }
200  if (isset($validate)) {
201  $this->validate = $validate;
202  }
203  if (isset($limit)) {
204  $this->limit = $limit;
205  }
206 
207  $this->structure = array();
208  $this->addresses = array();
209  $this->error = null;
210  $this->index = null;
211 
212  // Unfold any long lines in $this->address.
213  $this->address = preg_replace('/\r?\n/', "\r\n", $this->address);
214  $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address);
215 
216  while ($this->address = $this->_splitAddresses($this->address));
217 
218  if ($this->address === false || isset($this->error)) {
219  // mjansen patch 14 Ap 2016 start
220  throw new \ilMailException($this->error);
221  // mjansen patch 14 Ap 2016 end
222  }
223 
224  // Validate each address individually. If we encounter an invalid
225  // address, stop iterating and return an error immediately.
226  foreach ($this->addresses as $address) {
227  $valid = $this->_validateAddress($address);
228 
229  if ($valid === false || isset($this->error)) {
230  // mjansen patch 14 Ap 2016 start
231  throw new \ilMailException($this->error);
232  // mjansen patch 14 Ap 2016 end
233  }
234 
235  if (!$this->nestGroups) {
236  $this->structure = array_merge($this->structure, $valid);
237  } else {
238  $this->structure[] = $valid;
239  }
240  }
241 
242  return $this->structure;
243  }
244 
251  protected function _splitAddresses($address)
252  {
253  if (!empty($this->limit) && count($this->addresses) == $this->limit) {
254  return '';
255  }
256 
257  if ($this->_isGroup($address) && !isset($this->error)) {
258  $split_char = ';';
259  $is_group = true;
260  } elseif (!isset($this->error)) {
261  $split_char = ',';
262  $is_group = false;
263  } elseif (isset($this->error)) {
264  return false;
265  }
266 
267  // Split the string based on the above ten or so lines.
268  $parts = explode($split_char, $address);
269  $string = $this->_splitCheck($parts, $split_char);
270 
271  // If a group...
272  if ($is_group) {
273  // If $string does not contain a colon outside of
274  // brackets/quotes etc then something's fubar.
275 
276  // First check there's a colon at all:
277  if (strpos($string, ':') === false) {
278  $this->error = 'Invalid address: ' . $string;
279  return false;
280  }
281 
282  // Now check it's outside of brackets/quotes:
283  if (!$this->_splitCheck(explode(':', $string), ':')) {
284  return false;
285  }
286 
287  // We must have a group at this point, so increase the counter:
288  $this->num_groups++;
289  }
290 
291  // $string now contains the first full address/group.
292  // Add to the addresses array.
293  $this->addresses[] = array(
294  'address' => trim($string),
295  'group' => $is_group
296  );
297 
298  // Remove the now stored address from the initial line, the +1
299  // is to account for the explode character.
300  $address = trim(substr($address, strlen($string) + 1));
301 
302  // If the next char is a comma and this was a group, then
303  // there are more addresses, otherwise, if there are any more
304  // chars, then there is another address.
305  if ($is_group && substr($address, 0, 1) == ',') {
306  $address = trim(substr($address, 1));
307  return $address;
308  } elseif (strlen($address) > 0) {
309  return $address;
310  } else {
311  return '';
312  }
313 
314  // If you got here then something's off
315  return false;
316  }
317 
324  protected function _isGroup($address)
325  {
326  // First comma not in quotes, angles or escaped:
327  $parts = explode(',', $address);
328  $string = $this->_splitCheck($parts, ',');
329 
330  // Now we have the first address, we can reliably check for a
331  // group by searching for a colon that's not escaped or in
332  // quotes or angle brackets.
333  if (count($parts = explode(':', $string)) > 1) {
334  $string2 = $this->_splitCheck($parts, ':');
335  return ($string2 !== $string);
336  } else {
337  return false;
338  }
339  }
340 
348  protected function _splitCheck($parts, $char)
349  {
350  $string = $parts[0];
351 
352  for ($i = 0; $i < count($parts); $i++) {
353  if ($this->_hasUnclosedQuotes($string)
354  || $this->_hasUnclosedBrackets($string, '<>')
355  || $this->_hasUnclosedBrackets($string, '[]')
356  || $this->_hasUnclosedBrackets($string, '()')
357  || substr($string, -1) == '\\') {
358  if (isset($parts[$i + 1])) {
359  $string = $string . $char . $parts[$i + 1];
360  } else {
361  $this->error = 'Invalid address spec. Unclosed bracket or quotes';
362  return false;
363  }
364  } else {
365  $this->index = $i;
366  break;
367  }
368  }
369 
370  return $string;
371  }
372 
380  protected function _hasUnclosedQuotes($string)
381  {
382  $string = trim($string);
383  $iMax = strlen($string);
384  $in_quote = false;
385  $i = $slashes = 0;
386 
387  for (; $i < $iMax; ++$i) {
388  switch ($string[$i]) {
389  case '\\':
390  ++$slashes;
391  break;
392 
393  case '"':
394  if ($slashes % 2 == 0) {
395  $in_quote = !$in_quote;
396  }
397  // Fall through to default action below.
398 
399  // no break
400  default:
401  $slashes = 0;
402  break;
403  }
404  }
405 
406  return $in_quote;
407  }
408 
417  protected function _hasUnclosedBrackets($string, $chars)
418  {
419  $num_angle_start = substr_count($string, $chars[0]);
420  $num_angle_end = substr_count($string, $chars[1]);
421 
422  $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
423  $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
424 
425  if ($num_angle_start < $num_angle_end) {
426  $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
427  return false;
428  } else {
429  return ($num_angle_start > $num_angle_end);
430  }
431  }
432 
441  protected function _hasUnclosedBracketsSub($string, &$num, $char)
442  {
443  $parts = explode($char, $string);
444  for ($i = 0; $i < count($parts); $i++) {
445  if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) {
446  $num--;
447  }
448  if (isset($parts[$i + 1])) {
449  $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
450  }
451  }
452 
453  return $num;
454  }
455 
462  protected function _validateAddress($address)
463  {
464  $is_group = false;
465  $addresses = array();
466 
467  if ($address['group']) {
468  $is_group = true;
469 
470  // Get the group part of the name
471  $parts = explode(':', $address['address']);
472  $groupname = $this->_splitCheck($parts, ':');
473  $structure = array();
474 
475  // And validate the group part of the name.
476  if (!$this->_validatePhrase($groupname)) {
477  $this->error = 'Group name did not validate.';
478  return false;
479  } else {
480  // Don't include groups if we are not nesting
481  // them. This avoids returning invalid addresses.
482  if ($this->nestGroups) {
483  $structure = new stdClass;
484  $structure->groupname = $groupname;
485  }
486  }
487 
488  $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
489  }
490 
491  // If a group then split on comma and put into an array.
492  // Otherwise, Just put the whole address in an array.
493  if ($is_group) {
494  while (strlen($address['address']) > 0) {
495  $parts = explode(',', $address['address']);
496  $addresses[] = $this->_splitCheck($parts, ',');
497  $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
498  }
499  } else {
500  $addresses[] = $address['address'];
501  }
502 
503  // Trim the whitespace from all of the address strings.
504  array_map('trim', $addresses);
505 
506  // Validate each mailbox.
507  // Format could be one of: name <geezer@domain.com>
508  // geezer@domain.com
509  // geezer
510  // ... or any other format valid by RFC 822.
511  for ($i = 0; $i < count($addresses); $i++) {
512  if (!$this->validateMailbox($addresses[$i])) {
513  if (empty($this->error)) {
514  $this->error = 'Validation failed for: ' . $addresses[$i];
515  }
516  return false;
517  }
518  }
519 
520  // Nested format
521  if ($this->nestGroups) {
522  if ($is_group) {
523  $structure->addresses = $addresses;
524  } else {
525  $structure = $addresses[0];
526  }
527 
528  // Flat format
529  } else {
530  if ($is_group) {
531  $structure = array_merge($structure, $addresses);
532  } else {
534  }
535  }
536 
537  return $structure;
538  }
539 
546  protected function _validatePhrase($phrase)
547  {
548  // Splits on one or more Tab or space.
549  $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
550 
551  $phrase_parts = array();
552  while (count($parts) > 0) {
553  $phrase_parts[] = $this->_splitCheck($parts, ' ');
554  for ($i = 0; $i < $this->index + 1; $i++) {
555  array_shift($parts);
556  }
557  }
558 
559  foreach ($phrase_parts as $part) {
560  // If quoted string:
561  if (substr($part, 0, 1) == '"') {
562  if (!$this->_validateQuotedString($part)) {
563  return false;
564  }
565  continue;
566  }
567 
568  // Otherwise it's an atom:
569  if (!$this->_validateAtom($part)) {
570  return false;
571  }
572  }
573 
574  return true;
575  }
576 
589  protected function _validateAtom($atom)
590  {
591  if (!$this->validate) {
592  // Validation has been turned off; assume the atom is okay.
593  return true;
594  }
595 
596  // Check for any char from ASCII 0 - ASCII 127
597  // mjansen patch 16 Sep 2015 start
598  // Check for specials:
599  if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
600  return false;
601  }
602 
603  // Check for control characters (ASCII 0-31):
604  if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
605  return false;
606  }
607  #16291
608  #17618
609  if (!(bool) preg_match('//u', $atom)) {
610  return false;
611  }
612  // mjansen patch 16 Sep 2015 end
613 
614  return true;
615  }
616 
624  protected function _validateQuotedString($qstring)
625  {
626  // Leading and trailing "
627  $qstring = substr($qstring, 1, -1);
628 
629  // Perform check, removing quoted characters first.
630  return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
631  }
632 
641  public function validateMailbox(&$mailbox)
642  {
643  // A couple of defaults.
644  $phrase = '';
645  $comment = '';
646  $comments = array();
647 
648  // Catch any RFC822 comments and store them separately.
649  $_mailbox = $mailbox;
650  while (strlen(trim($_mailbox)) > 0) {
651  $parts = explode('(', $_mailbox);
652  $before_comment = $this->_splitCheck($parts, '(');
653  if ($before_comment != $_mailbox) {
654  // First char should be a (.
655  $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
656  $parts = explode(')', $comment);
657  $comment = $this->_splitCheck($parts, ')');
658  $comments[] = $comment;
659 
660  // +2 is for the brackets
661  $_mailbox = substr($_mailbox, strpos($_mailbox, '(' . $comment) + strlen($comment) + 2);
662  } else {
663  break;
664  }
665  }
666 
667  foreach ($comments as $comment) {
668  $mailbox = str_replace("($comment)", '', $mailbox);
669  }
670 
671  $mailbox = trim($mailbox);
672 
673  // Check for name + route-addr
674  if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') {
675  $parts = explode('<', $mailbox);
676  $name = $this->_splitCheck($parts, '<');
677 
678  $phrase = trim($name);
679  $route_addr = trim(substr($mailbox, strlen($name . '<'), -1));
680 
681  if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {
682  return false;
683  }
684 
685  // Only got addr-spec
686  } else {
687  // First snip angle brackets if present.
688  if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') {
689  $addr_spec = substr($mailbox, 1, -1);
690  } else {
691  $addr_spec = $mailbox;
692  }
693 
694  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
695  return false;
696  }
697  }
698 
699  // Construct the object that will be returned.
700  $mbox = new stdClass();
701 
702  // Add the phrase (even if empty) and comments
703  $mbox->personal = $phrase;
704  $mbox->comment = isset($comments) ? $comments : array();
705 
706  if (isset($route_addr)) {
707  $mbox->mailbox = $route_addr['local_part'];
708  $mbox->host = $route_addr['domain'];
709  $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';
710  } else {
711  $mbox->mailbox = $addr_spec['local_part'];
712  $mbox->host = $addr_spec['domain'];
713  }
714 
715  $mailbox = $mbox;
716  return true;
717  }
718 
729  protected function _validateRouteAddr($route_addr)
730  {
731  // Check for colon.
732  if (strpos($route_addr, ':') !== false) {
733  $parts = explode(':', $route_addr);
734  $route = $this->_splitCheck($parts, ':');
735  } else {
736  $route = $route_addr;
737  }
738 
739  // If $route is same as $route_addr then the colon was in
740  // quotes or brackets or, of course, non existent.
741  if ($route === $route_addr) {
742  unset($route);
743  $addr_spec = $route_addr;
744  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
745  return false;
746  }
747  } else {
748  // Validate route part.
749  if (($route = $this->_validateRoute($route)) === false) {
750  return false;
751  }
752 
753  $addr_spec = substr($route_addr, strlen($route . ':'));
754 
755  // Validate addr-spec part.
756  if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
757  return false;
758  }
759  }
760 
761  if (isset($route)) {
762  $return['adl'] = $route;
763  } else {
764  $return['adl'] = '';
765  }
766 
767  $return = array_merge($return, $addr_spec);
768  return $return;
769  }
770 
778  protected function _validateRoute($route)
779  {
780  // Split on comma.
781  $domains = explode(',', trim($route));
782 
783  foreach ($domains as $domain) {
784  $domain = str_replace('@', '', trim($domain));
785  if (!$this->_validateDomain($domain)) {
786  return false;
787  }
788  }
789 
790  return $route;
791  }
792 
802  protected function _validateDomain($domain)
803  {
804  // Note the different use of $subdomains and $sub_domains
805  $subdomains = explode('.', $domain);
806 
807  while (count($subdomains) > 0) {
808  $sub_domains[] = $this->_splitCheck($subdomains, '.');
809  for ($i = 0; $i < $this->index + 1; $i++) {
810  array_shift($subdomains);
811  }
812  }
813 
814  foreach ($sub_domains as $sub_domain) {
815  if (!$this->_validateSubdomain(trim($sub_domain))) {
816  return false;
817  }
818  }
819 
820  // Managed to get here, so return input.
821  return $domain;
822  }
823 
831  protected function _validateSubdomain($subdomain)
832  {
833  if (preg_match('|^\[(.*)]$|', $subdomain, $arr)) {
834  if (!$this->_validateDliteral($arr[1])) {
835  return false;
836  }
837  } else {
838  if (!$this->_validateAtom($subdomain)) {
839  return false;
840  }
841  }
842 
843  // Got here, so return successful.
844  return true;
845  }
846 
854  protected function _validateDliteral($dliteral)
855  {
856  return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && ((!isset($matches[1])) || $matches[1] != '\\');
857  }
858 
867  protected function _validateAddrSpec($addr_spec)
868  {
869  $addr_spec = trim($addr_spec);
870 
871  // mjansen patch 16 Sep 2016 start
872  $validateState = $this->validate;
873  // mjansen patch 16 Sep 2016 end
874  // Split on @ sign if there is one.
875  if (strpos($addr_spec, '@') !== false) {
876  $parts = explode('@', $addr_spec);
877  $local_part = $this->_splitCheck($parts, '@');
878  $domain = substr($addr_spec, strlen($local_part . '@'));
879  // mjansen patch 16 Sep 2016 start
880  if (substr_count($addr_spec, '@') != 1 && $local_part == '') {
881  $this->validate = false;
882  $local_part = $addr_spec;
884  }
885  // mjansen patch 16 Sep 2016 end
886  // No @ sign so assume the default domain.
887  } else {
888  $local_part = $addr_spec;
890  }
891 
892  if (($local_part = $this->_validateLocalPart($local_part)) === false) {
893  return false;
894  }
895  // mjansen patch 16 Sep 2016 start
896  if ($validateState != $this->validate) {
897  $this->validate = $validateState;
898  }
899  // mjansen patch 16 Sep 2016 end
900  if (($domain = $this->_validateDomain($domain)) === false) {
901  return false;
902  }
903 
904  // Got here so return successful.
905  return array('local_part' => $local_part, 'domain' => $domain);
906  }
907 
916  protected function _validateLocalPart($local_part)
917  {
918  $parts = explode('.', $local_part);
919  $words = array();
920 
921  // Split the local_part into words.
922  while (count($parts) > 0) {
923  $words[] = $this->_splitCheck($parts, '.');
924  for ($i = 0; $i < $this->index + 1; $i++) {
925  array_shift($parts);
926  }
927  }
928 
929  // Validate each word.
930  foreach ($words as $word) {
931  // iszmais patch 19 May 2020 start
932  // word cannot be empty (#17317)
933  //if ($word === '') {
934  // return false;
935  //}
936  // iszmais patch 19 May 2020 end
937  // If this word contains an unquoted space, it is invalid. (6.2.4)
938  if (strpos($word, ' ') && $word[0] !== '"') {
939  // mjansen patch 24 Feb 2016 start
940  // Mantis issue #18018
941  // # http://haacked.com/archive/2007/08/21/i-knew-how-to-validate-an-email-address-until-i.aspx/
942  //return false;
943  // mjansen patch 24 Feb 2016 end
944  }
945 
946  if ($this->_validatePhrase(trim($word)) === false) {
947  return false;
948  }
949  }
950 
951  // Managed to get here, so return the input.
952  return $local_part;
953  }
954 
965  public function approximateCount($data)
966  {
967  return count(preg_split('/(?<!\\\\),/', $data));
968  }
969 
983  public function isValidInetAddress($data, $strict = false)
984  {
985  $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i';
986  if (preg_match($regex, trim($data), $matches)) {
987  return array($matches[1], $matches[2]);
988  } else {
989  return false;
990  }
991  }
992 }
$index
An internal counter/pointer.
Definition: RFC822.php:121
_validateDliteral($dliteral)
Function to validate a domain literal: domain-literal = "[" *(dtext / quoted-pair) "]"...
Definition: RFC822.php:854
$validate
Whether or not to validate atoms for non-ascii characters.
Definition: RFC822.php:97
_hasUnclosedBrackets($string, $chars)
Checks if a string has an unclosed brackets or not.
Definition: RFC822.php:417
_validateRoute($route)
Function to validate a route, which is: route = 1#("@" domain) ":".
Definition: RFC822.php:778
_validateAddrSpec($addr_spec)
Function to validate an addr-spec.
Definition: RFC822.php:867
$valid
$default_domain
The default domain to use for unqualified addresses.
Definition: RFC822.php:85
_hasUnclosedBracketsSub($string, &$num, $char)
Sub function that is used only by hasUnclosedBrackets().
Definition: RFC822.php:441
parseAddressList($address=null, $default_domain=null, $nest_groups=null, $validate=null, $limit=null)
Starts the whole process.
Definition: RFC822.php:184
_validateAddress($address)
Function to begin checking the address.
Definition: RFC822.php:462
_validateDomain($domain)
Function to validate a domain, though this is not quite what you expect of a strict internet domain...
Definition: RFC822.php:802
$structure
The final array of parsed address information that we build up.
Definition: RFC822.php:109
_splitCheck($parts, $char)
A common function that will check an exploded string.
Definition: RFC822.php:348
_validatePhrase($phrase)
Function to validate a phrase.
Definition: RFC822.php:546
$limit
A limit after which processing stops.
Definition: RFC822.php:141
$error
The current error message, if any.
Definition: RFC822.php:115
_splitAddresses($address)
Splits an address into separate addresses.
Definition: RFC822.php:251
_validateRouteAddr($route_addr)
This function validates a route-addr which is: route-addr = "<" [route] addr-spec ">"...
Definition: RFC822.php:729
_validateQuotedString($qstring)
Function to validate quoted string, which is: quoted-string = <"> *(qtext/quoted-pair) <"> ...
Definition: RFC822.php:624
$num_groups
The number of groups that have been found in the address list.
Definition: RFC822.php:128
approximateCount($data)
Returns an approximate count of how many addresses are in the given string.
Definition: RFC822.php:965
_hasUnclosedQuotes($string)
Checks if a string has unclosed quotes or not.
Definition: RFC822.php:380
validateMailbox(&$mailbox)
Function to validate a mailbox, which is: mailbox = addr-spec ; simple address / phrase route-addr ; ...
Definition: RFC822.php:641
$comment
Definition: buildRTE.php:83
_validateLocalPart($local_part)
Function to validate the local part of an address: local-part = word *("." word)
Definition: RFC822.php:916
$mailRFC822
A variable so that we can tell whether or not we&#39;re inside a Mail_RFC822 object.
Definition: RFC822.php:135
$address
The address being parsed by the RFC822 object.
Definition: RFC822.php:79
isValidInetAddress($data, $strict=false)
This is a email validating function separate to the rest of the class.
Definition: RFC822.php:983
$nestGroups
Should we return a nested array showing groups, or flatten everything?
Definition: RFC822.php:91
$i
Definition: disco.tpl.php:19
_isGroup($address)
Checks for a group at the start of the string.
Definition: RFC822.php:324
if(!array_key_exists('domain', $_REQUEST)) $domain
Definition: resume.php:8
__construct($address=null, $default_domain=null, $nest_groups=null, $validate=null, $limit=null)
Sets up the object.
Definition: RFC822.php:154
_validateSubdomain($subdomain)
Function to validate a subdomain: subdomain = domain-ref / domain-literal.
Definition: RFC822.php:831
$addresses
The array of raw addresses built up as we parse.
Definition: RFC822.php:103
_validateAtom($atom)
Function to validate an atom which from rfc822 is: atom = 1*<any CHAR except specials, SPACE and CTLs>
Definition: RFC822.php:589
$data
Definition: bench.php:6