PageRenderTime 66ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/PEAR/Mail/RFC822.php

https://bitbucket.org/kucing2k/ediassoc
PHP | 923 lines | 516 code | 96 blank | 311 comment | 110 complexity | 53709b86035634a8095fecbeb5d67a7f MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1, BSD-2-Clause, GPL-2.0
  1. <?php
  2. // +-----------------------------------------------------------------------+
  3. // | Copyright (c) 2001-2002, Richard Heyes |
  4. // | All rights reserved. |
  5. // | |
  6. // | Redistribution and use in source and binary forms, with or without |
  7. // | modification, are permitted provided that the following conditions |
  8. // | are met: |
  9. // | |
  10. // | o Redistributions of source code must retain the above copyright |
  11. // | notice, this list of conditions and the following disclaimer. |
  12. // | o Redistributions in binary form must reproduce the above copyright |
  13. // | notice, this list of conditions and the following disclaimer in the |
  14. // | documentation and/or other materials provided with the distribution.|
  15. // | o The names of the authors may not be used to endorse or promote |
  16. // | products derived from this software without specific prior written |
  17. // | permission. |
  18. // | |
  19. // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
  20. // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
  21. // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
  22. // | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
  23. // | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
  24. // | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
  25. // | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
  26. // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
  27. // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
  28. // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
  29. // | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
  30. // | |
  31. // +-----------------------------------------------------------------------+
  32. // | Authors: Richard Heyes <richard@phpguru.org> |
  33. // | Chuck Hagenbuch <chuck@horde.org> |
  34. // +-----------------------------------------------------------------------+
  35. /**
  36. * RFC 822 Email address list validation Utility
  37. *
  38. * What is it?
  39. *
  40. * This class will take an address string, and parse it into it's consituent
  41. * parts, be that either addresses, groups, or combinations. Nested groups
  42. * are not supported. The structure it returns is pretty straight forward,
  43. * and is similar to that provided by the imap_rfc822_parse_adrlist(). Use
  44. * print_r() to view the structure.
  45. *
  46. * How do I use it?
  47. *
  48. * $address_string = 'My Group: "Richard" <richard@localhost> (A comment), ted@example.com (Ted Bloggs), Barney;';
  49. * $structure = Mail_RFC822::parseAddressList($address_string, 'example.com', true)
  50. * print_r($structure);
  51. *
  52. * @author Richard Heyes <richard@phpguru.org>
  53. * @author Chuck Hagenbuch <chuck@horde.org>
  54. * @version $Revision: 1.22 $
  55. * @license BSD
  56. * @package Mail
  57. */
  58. class Mail_RFC822 {
  59. /**
  60. * The address being parsed by the RFC822 object.
  61. * @var string $address
  62. */
  63. var $address = '';
  64. /**
  65. * The default domain to use for unqualified addresses.
  66. * @var string $default_domain
  67. */
  68. var $default_domain = 'localhost';
  69. /**
  70. * Should we return a nested array showing groups, or flatten everything?
  71. * @var boolean $nestGroups
  72. */
  73. var $nestGroups = true;
  74. /**
  75. * Whether or not to validate atoms for non-ascii characters.
  76. * @var boolean $validate
  77. */
  78. var $validate = true;
  79. /**
  80. * The array of raw addresses built up as we parse.
  81. * @var array $addresses
  82. */
  83. var $addresses = array();
  84. /**
  85. * The final array of parsed address information that we build up.
  86. * @var array $structure
  87. */
  88. var $structure = array();
  89. /**
  90. * The current error message, if any.
  91. * @var string $error
  92. */
  93. var $error = null;
  94. /**
  95. * An internal counter/pointer.
  96. * @var integer $index
  97. */
  98. var $index = null;
  99. /**
  100. * The number of groups that have been found in the address list.
  101. * @var integer $num_groups
  102. * @access public
  103. */
  104. var $num_groups = 0;
  105. /**
  106. * A variable so that we can tell whether or not we're inside a
  107. * Mail_RFC822 object.
  108. * @var boolean $mailRFC822
  109. */
  110. var $mailRFC822 = true;
  111. /**
  112. * A limit after which processing stops
  113. * @var int $limit
  114. */
  115. var $limit = null;
  116. /**
  117. * Sets up the object. The address must either be set here or when
  118. * calling parseAddressList(). One or the other.
  119. *
  120. * @access public
  121. * @param string $address The address(es) to validate.
  122. * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost.
  123. * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing.
  124. * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
  125. *
  126. * @return object Mail_RFC822 A new Mail_RFC822 object.
  127. */
  128. function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
  129. {
  130. if (isset($address)) $this->address = $address;
  131. if (isset($default_domain)) $this->default_domain = $default_domain;
  132. if (isset($nest_groups)) $this->nestGroups = $nest_groups;
  133. if (isset($validate)) $this->validate = $validate;
  134. if (isset($limit)) $this->limit = $limit;
  135. }
  136. /**
  137. * Starts the whole process. The address must either be set here
  138. * or when creating the object. One or the other.
  139. *
  140. * @access public
  141. * @param string $address The address(es) to validate.
  142. * @param string $default_domain Default domain/host etc.
  143. * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing.
  144. * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance.
  145. *
  146. * @return array A structured array of addresses.
  147. */
  148. function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
  149. {
  150. if (!isset($this) || !isset($this->mailRFC822)) {
  151. $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit);
  152. return $obj->parseAddressList();
  153. }
  154. if (isset($address)) $this->address = $address;
  155. if (isset($default_domain)) $this->default_domain = $default_domain;
  156. if (isset($nest_groups)) $this->nestGroups = $nest_groups;
  157. if (isset($validate)) $this->validate = $validate;
  158. if (isset($limit)) $this->limit = $limit;
  159. $this->structure = array();
  160. $this->addresses = array();
  161. $this->error = null;
  162. $this->index = null;
  163. // Unfold any long lines in $this->address.
  164. $this->address = preg_replace('/\r?\n/', "\r\n", $this->address);
  165. $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address);
  166. while ($this->address = $this->_splitAddresses($this->address));
  167. if ($this->address === false || isset($this->error)) {
  168. require_once 'PEAR.php';
  169. return PEAR::raiseError($this->error);
  170. }
  171. // Validate each address individually. If we encounter an invalid
  172. // address, stop iterating and return an error immediately.
  173. foreach ($this->addresses as $address) {
  174. $valid = $this->_validateAddress($address);
  175. if ($valid === false || isset($this->error)) {
  176. require_once 'PEAR.php';
  177. return PEAR::raiseError($this->error);
  178. }
  179. if (!$this->nestGroups) {
  180. $this->structure = array_merge($this->structure, $valid);
  181. } else {
  182. $this->structure[] = $valid;
  183. }
  184. }
  185. return $this->structure;
  186. }
  187. /**
  188. * Splits an address into separate addresses.
  189. *
  190. * @access private
  191. * @param string $address The addresses to split.
  192. * @return boolean Success or failure.
  193. */
  194. function _splitAddresses($address)
  195. {
  196. if (!empty($this->limit) && count($this->addresses) == $this->limit) {
  197. return '';
  198. }
  199. if ($this->_isGroup($address) && !isset($this->error)) {
  200. $split_char = ';';
  201. $is_group = true;
  202. } elseif (!isset($this->error)) {
  203. $split_char = ',';
  204. $is_group = false;
  205. } elseif (isset($this->error)) {
  206. return false;
  207. }
  208. // Split the string based on the above ten or so lines.
  209. $parts = explode($split_char, $address);
  210. $string = $this->_splitCheck($parts, $split_char);
  211. // If a group...
  212. if ($is_group) {
  213. // If $string does not contain a colon outside of
  214. // brackets/quotes etc then something's fubar.
  215. // First check there's a colon at all:
  216. if (strpos($string, ':') === false) {
  217. $this->error = 'Invalid address: ' . $string;
  218. return false;
  219. }
  220. // Now check it's outside of brackets/quotes:
  221. if (!$this->_splitCheck(explode(':', $string), ':')) {
  222. return false;
  223. }
  224. // We must have a group at this point, so increase the counter:
  225. $this->num_groups++;
  226. }
  227. // $string now contains the first full address/group.
  228. // Add to the addresses array.
  229. $this->addresses[] = array(
  230. 'address' => trim($string),
  231. 'group' => $is_group
  232. );
  233. // Remove the now stored address from the initial line, the +1
  234. // is to account for the explode character.
  235. $address = trim(substr($address, strlen($string) + 1));
  236. // If the next char is a comma and this was a group, then
  237. // there are more addresses, otherwise, if there are any more
  238. // chars, then there is another address.
  239. if ($is_group && substr($address, 0, 1) == ','){
  240. $address = trim(substr($address, 1));
  241. return $address;
  242. } elseif (strlen($address) > 0) {
  243. return $address;
  244. } else {
  245. return '';
  246. }
  247. // If you got here then something's off
  248. return false;
  249. }
  250. /**
  251. * Checks for a group at the start of the string.
  252. *
  253. * @access private
  254. * @param string $address The address to check.
  255. * @return boolean Whether or not there is a group at the start of the string.
  256. */
  257. function _isGroup($address)
  258. {
  259. // First comma not in quotes, angles or escaped:
  260. $parts = explode(',', $address);
  261. $string = $this->_splitCheck($parts, ',');
  262. // Now we have the first address, we can reliably check for a
  263. // group by searching for a colon that's not escaped or in
  264. // quotes or angle brackets.
  265. if (count($parts = explode(':', $string)) > 1) {
  266. $string2 = $this->_splitCheck($parts, ':');
  267. return ($string2 !== $string);
  268. } else {
  269. return false;
  270. }
  271. }
  272. /**
  273. * A common function that will check an exploded string.
  274. *
  275. * @access private
  276. * @param array $parts The exloded string.
  277. * @param string $char The char that was exploded on.
  278. * @return mixed False if the string contains unclosed quotes/brackets, or the string on success.
  279. */
  280. function _splitCheck($parts, $char)
  281. {
  282. $string = $parts[0];
  283. for ($i = 0; $i < count($parts); $i++) {
  284. if ($this->_hasUnclosedQuotes($string)
  285. || $this->_hasUnclosedBrackets($string, '<>')
  286. || $this->_hasUnclosedBrackets($string, '[]')
  287. || $this->_hasUnclosedBrackets($string, '()')
  288. || substr($string, -1) == '\\') {
  289. if (isset($parts[$i + 1])) {
  290. $string = $string . $char . $parts[$i + 1];
  291. } else {
  292. $this->error = 'Invalid address spec. Unclosed bracket or quotes';
  293. return false;
  294. }
  295. } else {
  296. $this->index = $i;
  297. break;
  298. }
  299. }
  300. return $string;
  301. }
  302. /**
  303. * Checks if a string has an unclosed quotes or not.
  304. *
  305. * @access private
  306. * @param string $string The string to check.
  307. * @return boolean True if there are unclosed quotes inside the string, false otherwise.
  308. */
  309. function _hasUnclosedQuotes($string)
  310. {
  311. $string = explode('"', $string);
  312. $string_cnt = count($string);
  313. for ($i = 0; $i < (count($string) - 1); $i++)
  314. if (substr($string[$i], -1) == '\\')
  315. $string_cnt--;
  316. return ($string_cnt % 2 === 0);
  317. }
  318. /**
  319. * Checks if a string has an unclosed brackets or not. IMPORTANT:
  320. * This function handles both angle brackets and square brackets;
  321. *
  322. * @access private
  323. * @param string $string The string to check.
  324. * @param string $chars The characters to check for.
  325. * @return boolean True if there are unclosed brackets inside the string, false otherwise.
  326. */
  327. function _hasUnclosedBrackets($string, $chars)
  328. {
  329. $num_angle_start = substr_count($string, $chars[0]);
  330. $num_angle_end = substr_count($string, $chars[1]);
  331. $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]);
  332. $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]);
  333. if ($num_angle_start < $num_angle_end) {
  334. $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')';
  335. return false;
  336. } else {
  337. return ($num_angle_start > $num_angle_end);
  338. }
  339. }
  340. /**
  341. * Sub function that is used only by hasUnclosedBrackets().
  342. *
  343. * @access private
  344. * @param string $string The string to check.
  345. * @param integer &$num The number of occurences.
  346. * @param string $char The character to count.
  347. * @return integer The number of occurences of $char in $string, adjusted for backslashes.
  348. */
  349. function _hasUnclosedBracketsSub($string, &$num, $char)
  350. {
  351. $parts = explode($char, $string);
  352. for ($i = 0; $i < count($parts); $i++){
  353. if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i]))
  354. $num--;
  355. if (isset($parts[$i + 1]))
  356. $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1];
  357. }
  358. return $num;
  359. }
  360. /**
  361. * Function to begin checking the address.
  362. *
  363. * @access private
  364. * @param string $address The address to validate.
  365. * @return mixed False on failure, or a structured array of address information on success.
  366. */
  367. function _validateAddress($address)
  368. {
  369. $is_group = false;
  370. $addresses = array();
  371. if ($address['group']) {
  372. $is_group = true;
  373. // Get the group part of the name
  374. $parts = explode(':', $address['address']);
  375. $groupname = $this->_splitCheck($parts, ':');
  376. $structure = array();
  377. // And validate the group part of the name.
  378. if (!$this->_validatePhrase($groupname)){
  379. $this->error = 'Group name did not validate.';
  380. return false;
  381. } else {
  382. // Don't include groups if we are not nesting
  383. // them. This avoids returning invalid addresses.
  384. if ($this->nestGroups) {
  385. $structure = new stdClass;
  386. $structure->groupname = $groupname;
  387. }
  388. }
  389. $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':')));
  390. }
  391. // If a group then split on comma and put into an array.
  392. // Otherwise, Just put the whole address in an array.
  393. if ($is_group) {
  394. while (strlen($address['address']) > 0) {
  395. $parts = explode(',', $address['address']);
  396. $addresses[] = $this->_splitCheck($parts, ',');
  397. $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ',')));
  398. }
  399. } else {
  400. $addresses[] = $address['address'];
  401. }
  402. // Check that $addresses is set, if address like this:
  403. // Groupname:;
  404. // Then errors were appearing.
  405. if (!count($addresses)){
  406. $this->error = 'Empty group.';
  407. return false;
  408. }
  409. // Trim the whitespace from all of the address strings.
  410. array_map('trim', $addresses);
  411. // Validate each mailbox.
  412. // Format could be one of: name <geezer@domain.com>
  413. // geezer@domain.com
  414. // geezer
  415. // ... or any other format valid by RFC 822.
  416. for ($i = 0; $i < count($addresses); $i++) {
  417. if (!$this->validateMailbox($addresses[$i])) {
  418. if (empty($this->error)) {
  419. $this->error = 'Validation failed for: ' . $addresses[$i];
  420. }
  421. return false;
  422. }
  423. }
  424. // Nested format
  425. if ($this->nestGroups) {
  426. if ($is_group) {
  427. $structure->addresses = $addresses;
  428. } else {
  429. $structure = $addresses[0];
  430. }
  431. // Flat format
  432. } else {
  433. if ($is_group) {
  434. $structure = array_merge($structure, $addresses);
  435. } else {
  436. $structure = $addresses;
  437. }
  438. }
  439. return $structure;
  440. }
  441. /**
  442. * Function to validate a phrase.
  443. *
  444. * @access private
  445. * @param string $phrase The phrase to check.
  446. * @return boolean Success or failure.
  447. */
  448. function _validatePhrase($phrase)
  449. {
  450. // Splits on one or more Tab or space.
  451. $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY);
  452. $phrase_parts = array();
  453. while (count($parts) > 0){
  454. $phrase_parts[] = $this->_splitCheck($parts, ' ');
  455. for ($i = 0; $i < $this->index + 1; $i++)
  456. array_shift($parts);
  457. }
  458. foreach ($phrase_parts as $part) {
  459. // If quoted string:
  460. if (substr($part, 0, 1) == '"') {
  461. if (!$this->_validateQuotedString($part)) {
  462. return false;
  463. }
  464. continue;
  465. }
  466. // Otherwise it's an atom:
  467. if (!$this->_validateAtom($part)) return false;
  468. }
  469. return true;
  470. }
  471. /**
  472. * Function to validate an atom which from rfc822 is:
  473. * atom = 1*<any CHAR except specials, SPACE and CTLs>
  474. *
  475. * If validation ($this->validate) has been turned off, then
  476. * validateAtom() doesn't actually check anything. This is so that you
  477. * can split a list of addresses up before encoding personal names
  478. * (umlauts, etc.), for example.
  479. *
  480. * @access private
  481. * @param string $atom The string to check.
  482. * @return boolean Success or failure.
  483. */
  484. function _validateAtom($atom)
  485. {
  486. if (!$this->validate) {
  487. // Validation has been turned off; assume the atom is okay.
  488. return true;
  489. }
  490. // Check for any char from ASCII 0 - ASCII 127
  491. if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) {
  492. return false;
  493. }
  494. // Check for specials:
  495. if (preg_match('/[][()<>@,;\\:". ]/', $atom)) {
  496. return false;
  497. }
  498. // Check for control characters (ASCII 0-31):
  499. if (preg_match('/[\\x00-\\x1F]+/', $atom)) {
  500. return false;
  501. }
  502. return true;
  503. }
  504. /**
  505. * Function to validate quoted string, which is:
  506. * quoted-string = <"> *(qtext/quoted-pair) <">
  507. *
  508. * @access private
  509. * @param string $qstring The string to check
  510. * @return boolean Success or failure.
  511. */
  512. function _validateQuotedString($qstring)
  513. {
  514. // Leading and trailing "
  515. $qstring = substr($qstring, 1, -1);
  516. // Perform check, removing quoted characters first.
  517. return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring));
  518. }
  519. /**
  520. * Function to validate a mailbox, which is:
  521. * mailbox = addr-spec ; simple address
  522. * / phrase route-addr ; name and route-addr
  523. *
  524. * @access public
  525. * @param string &$mailbox The string to check.
  526. * @return boolean Success or failure.
  527. */
  528. function validateMailbox(&$mailbox)
  529. {
  530. // A couple of defaults.
  531. $phrase = '';
  532. $comment = '';
  533. $comments = array();
  534. // Catch any RFC822 comments and store them separately.
  535. $_mailbox = $mailbox;
  536. while (strlen(trim($_mailbox)) > 0) {
  537. $parts = explode('(', $_mailbox);
  538. $before_comment = $this->_splitCheck($parts, '(');
  539. if ($before_comment != $_mailbox) {
  540. // First char should be a (.
  541. $comment = substr(str_replace($before_comment, '', $_mailbox), 1);
  542. $parts = explode(')', $comment);
  543. $comment = $this->_splitCheck($parts, ')');
  544. $comments[] = $comment;
  545. // +1 is for the trailing )
  546. $_mailbox = substr($_mailbox, strpos($_mailbox, $comment)+strlen($comment)+1);
  547. } else {
  548. break;
  549. }
  550. }
  551. foreach ($comments as $comment) {
  552. $mailbox = str_replace("($comment)", '', $mailbox);
  553. }
  554. $mailbox = trim($mailbox);
  555. // Check for name + route-addr
  556. if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') {
  557. $parts = explode('<', $mailbox);
  558. $name = $this->_splitCheck($parts, '<');
  559. $phrase = trim($name);
  560. $route_addr = trim(substr($mailbox, strlen($name.'<'), -1));
  561. if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) {
  562. return false;
  563. }
  564. // Only got addr-spec
  565. } else {
  566. // First snip angle brackets if present.
  567. if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') {
  568. $addr_spec = substr($mailbox, 1, -1);
  569. } else {
  570. $addr_spec = $mailbox;
  571. }
  572. if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
  573. return false;
  574. }
  575. }
  576. // Construct the object that will be returned.
  577. $mbox = new stdClass();
  578. // Add the phrase (even if empty) and comments
  579. $mbox->personal = $phrase;
  580. $mbox->comment = isset($comments) ? $comments : array();
  581. if (isset($route_addr)) {
  582. $mbox->mailbox = $route_addr['local_part'];
  583. $mbox->host = $route_addr['domain'];
  584. $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : '';
  585. } else {
  586. $mbox->mailbox = $addr_spec['local_part'];
  587. $mbox->host = $addr_spec['domain'];
  588. }
  589. $mailbox = $mbox;
  590. return true;
  591. }
  592. /**
  593. * This function validates a route-addr which is:
  594. * route-addr = "<" [route] addr-spec ">"
  595. *
  596. * Angle brackets have already been removed at the point of
  597. * getting to this function.
  598. *
  599. * @access private
  600. * @param string $route_addr The string to check.
  601. * @return mixed False on failure, or an array containing validated address/route information on success.
  602. */
  603. function _validateRouteAddr($route_addr)
  604. {
  605. // Check for colon.
  606. if (strpos($route_addr, ':') !== false) {
  607. $parts = explode(':', $route_addr);
  608. $route = $this->_splitCheck($parts, ':');
  609. } else {
  610. $route = $route_addr;
  611. }
  612. // If $route is same as $route_addr then the colon was in
  613. // quotes or brackets or, of course, non existent.
  614. if ($route === $route_addr){
  615. unset($route);
  616. $addr_spec = $route_addr;
  617. if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
  618. return false;
  619. }
  620. } else {
  621. // Validate route part.
  622. if (($route = $this->_validateRoute($route)) === false) {
  623. return false;
  624. }
  625. $addr_spec = substr($route_addr, strlen($route . ':'));
  626. // Validate addr-spec part.
  627. if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) {
  628. return false;
  629. }
  630. }
  631. if (isset($route)) {
  632. $return['adl'] = $route;
  633. } else {
  634. $return['adl'] = '';
  635. }
  636. $return = array_merge($return, $addr_spec);
  637. return $return;
  638. }
  639. /**
  640. * Function to validate a route, which is:
  641. * route = 1#("@" domain) ":"
  642. *
  643. * @access private
  644. * @param string $route The string to check.
  645. * @return mixed False on failure, or the validated $route on success.
  646. */
  647. function _validateRoute($route)
  648. {
  649. // Split on comma.
  650. $domains = explode(',', trim($route));
  651. foreach ($domains as $domain) {
  652. $domain = str_replace('@', '', trim($domain));
  653. if (!$this->_validateDomain($domain)) return false;
  654. }
  655. return $route;
  656. }
  657. /**
  658. * Function to validate a domain, though this is not quite what
  659. * you expect of a strict internet domain.
  660. *
  661. * domain = sub-domain *("." sub-domain)
  662. *
  663. * @access private
  664. * @param string $domain The string to check.
  665. * @return mixed False on failure, or the validated domain on success.
  666. */
  667. function _validateDomain($domain)
  668. {
  669. // Note the different use of $subdomains and $sub_domains
  670. $subdomains = explode('.', $domain);
  671. while (count($subdomains) > 0) {
  672. $sub_domains[] = $this->_splitCheck($subdomains, '.');
  673. for ($i = 0; $i < $this->index + 1; $i++)
  674. array_shift($subdomains);
  675. }
  676. foreach ($sub_domains as $sub_domain) {
  677. if (!$this->_validateSubdomain(trim($sub_domain)))
  678. return false;
  679. }
  680. // Managed to get here, so return input.
  681. return $domain;
  682. }
  683. /**
  684. * Function to validate a subdomain:
  685. * subdomain = domain-ref / domain-literal
  686. *
  687. * @access private
  688. * @param string $subdomain The string to check.
  689. * @return boolean Success or failure.
  690. */
  691. function _validateSubdomain($subdomain)
  692. {
  693. if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){
  694. if (!$this->_validateDliteral($arr[1])) return false;
  695. } else {
  696. if (!$this->_validateAtom($subdomain)) return false;
  697. }
  698. // Got here, so return successful.
  699. return true;
  700. }
  701. /**
  702. * Function to validate a domain literal:
  703. * domain-literal = "[" *(dtext / quoted-pair) "]"
  704. *
  705. * @access private
  706. * @param string $dliteral The string to check.
  707. * @return boolean Success or failure.
  708. */
  709. function _validateDliteral($dliteral)
  710. {
  711. return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && $matches[1] != '\\';
  712. }
  713. /**
  714. * Function to validate an addr-spec.
  715. *
  716. * addr-spec = local-part "@" domain
  717. *
  718. * @access private
  719. * @param string $addr_spec The string to check.
  720. * @return mixed False on failure, or the validated addr-spec on success.
  721. */
  722. function _validateAddrSpec($addr_spec)
  723. {
  724. $addr_spec = trim($addr_spec);
  725. // Split on @ sign if there is one.
  726. if (strpos($addr_spec, '@') !== false) {
  727. $parts = explode('@', $addr_spec);
  728. $local_part = $this->_splitCheck($parts, '@');
  729. $domain = substr($addr_spec, strlen($local_part . '@'));
  730. // No @ sign so assume the default domain.
  731. } else {
  732. $local_part = $addr_spec;
  733. $domain = $this->default_domain;
  734. }
  735. if (($local_part = $this->_validateLocalPart($local_part)) === false) return false;
  736. if (($domain = $this->_validateDomain($domain)) === false) return false;
  737. // Got here so return successful.
  738. return array('local_part' => $local_part, 'domain' => $domain);
  739. }
  740. /**
  741. * Function to validate the local part of an address:
  742. * local-part = word *("." word)
  743. *
  744. * @access private
  745. * @param string $local_part
  746. * @return mixed False on failure, or the validated local part on success.
  747. */
  748. function _validateLocalPart($local_part)
  749. {
  750. $parts = explode('.', $local_part);
  751. $words = array();
  752. // Split the local_part into words.
  753. while (count($parts) > 0){
  754. $words[] = $this->_splitCheck($parts, '.');
  755. for ($i = 0; $i < $this->index + 1; $i++) {
  756. array_shift($parts);
  757. }
  758. }
  759. // Validate each word.
  760. foreach ($words as $word) {
  761. // If this word contains an unquoted space, it is invalid. (6.2.4)
  762. if (strpos($word, ' ') && $word[0] !== '"')
  763. {
  764. return false;
  765. }
  766. if ($this->_validatePhrase(trim($word)) === false) return false;
  767. }
  768. // Managed to get here, so return the input.
  769. return $local_part;
  770. }
  771. /**
  772. * Returns an approximate count of how many addresses are in the
  773. * given string. This is APPROXIMATE as it only splits based on a
  774. * comma which has no preceding backslash. Could be useful as
  775. * large amounts of addresses will end up producing *large*
  776. * structures when used with parseAddressList().
  777. *
  778. * @param string $data Addresses to count
  779. * @return int Approximate count
  780. */
  781. function approximateCount($data)
  782. {
  783. return count(preg_split('/(?<!\\\\),/', $data));
  784. }
  785. /**
  786. * This is a email validating function separate to the rest of the
  787. * class. It simply validates whether an email is of the common
  788. * internet form: <user>@<domain>. This can be sufficient for most
  789. * people. Optional stricter mode can be utilised which restricts
  790. * mailbox characters allowed to alphanumeric, full stop, hyphen
  791. * and underscore.
  792. *
  793. * @param string $data Address to check
  794. * @param boolean $strict Optional stricter mode
  795. * @return mixed False if it fails, an indexed array
  796. * username/domain if it matches
  797. */
  798. function isValidInetAddress($data, $strict = false)
  799. {
  800. $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,4})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,4})$/i';
  801. if (preg_match($regex, trim($data), $matches)) {
  802. return array($matches[1], $matches[2]);
  803. } else {
  804. return false;
  805. }
  806. }
  807. }