Guest User

flourish fMailbox Morg.

a guest
Mar 6th, 2012
261
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 53.01 KB | None | 0 0
  1. <?php
  2. /**
  3. * Retrieves and deletes messages from a email account via IMAP or POP3
  4. *
  5. * All headers, text and html content returned by this class are encoded in
  6. * UTF-8. Please see http://flourishlib.com/docs/UTF-8 for more information.
  7. *
  8. * @copyright Copyright (c) 2010-2011 Will Bond
  9. * @author Will Bond [wb] <will@flourishlib.com>
  10. * @license http://flourishlib.com/license
  11. *
  12. * @package Flourish
  13. * @link http://flourishlib.com/fMailbox
  14. *
  15. * @version 1.0.0b14
  16. * @changes 1.0.0b14 Added a workaround for iconv having issues in MAMP 1.9.4+ [wb, 2011-07-26]
  17. * @changes 1.0.0b13 Fixed handling of headers in relation to encoded-words being embedded inside of quoted strings [wb, 2011-07-26]
  18. * @changes 1.0.0b12 Enhanced the error checking in ::write() [wb, 2011-06-03]
  19. * @changes 1.0.0b11 Added code to work around PHP bug #42682 (http://bugs.php.net/bug.php?id=42682) where `stream_select()` doesn't work on 64bit machines from PHP 5.2.0 to 5.2.5, improved connectivity error handling and timeouts while reading data [wb, 2011-01-10]
  20. * @changes 1.0.0b10 Fixed ::parseMessage() to properly handle a header format edge case and properly set the `text` and `html` keys even when the email has an explicit `Content-disposition: inline` header [wb, 2010-11-25]
  21. * @changes 1.0.0b9 Fixed a bug in ::parseMessage() that could cause HTML alternate content to be included in the `inline` content array instead of the `html` element [wb, 2010-09-20]
  22. * @changes 1.0.0b8 Fixed ::parseMessage() to be able to handle non-text/non-html multipart parts that do not have a `Content-disposition` header [wb, 2010-09-18]
  23. * @changes 1.0.0b7 Fixed a typo in ::read() [wb, 2010-09-07]
  24. * @changes 1.0.0b6 Fixed a typo from 1.0.0b4 [wb, 2010-07-21]
  25. * @changes 1.0.0b5 Fixes for increased compatibility with various IMAP and POP3 servers, hacked around a bug in PHP 5.3 on Windows [wb, 2010-06-22]
  26. * @changes 1.0.0b4 Added code to handle emails without an explicit `Content-type` header [wb, 2010-06-04]
  27. * @changes 1.0.0b3 Added missing static method callback constants [wb, 2010-05-11]
  28. * @changes 1.0.0b2 Added the missing ::enableDebugging() [wb, 2010-05-05]
  29. * @changes 1.0.0b The initial implementation [wb, 2010-05-05]
  30. */
  31. class fMailbox
  32. {
  33. const addSMIMEPair = 'fMailbox::addSMIMEPair';
  34. const parseMessage = 'fMailbox::parseMessage';
  35. const reset = 'fMailbox::reset';
  36.  
  37.  
  38. /**
  39. * S/MIME certificates and private keys for verification and decryption
  40. *
  41. * @var array
  42. */
  43. static private $smime_pairs = array();
  44.  
  45.  
  46. /**
  47. * Adds an S/MIME certificate, or certificate + private key pair for verification and decryption of S/MIME messages
  48. *
  49. * @param string $email_address The email address the certificate or private key is for
  50. * @param fFile|string $certificate_file The file the S/MIME certificate is stored in - required for verification and decryption
  51. * @param fFile $private_key_file The file the S/MIME private key is stored in - required for decryption only
  52. * @param string $private_key_password The password for the private key
  53. * @return void
  54. */
  55. static public function addSMIMEPair($email_address, $certificate_file, $private_key_file=NULL, $private_key_password=NULL)
  56. {
  57. if ($private_key_file !== NULL && !$private_key_file instanceof fFile) {
  58. $private_key_file = new fFile($private_key_file);
  59. }
  60. if (!$certificate_file instanceof fFile) {
  61. $certificate_file = new fFile($certificate_file);
  62. }
  63. self::$smime_pairs[strtolower($email_address)] = array(
  64. 'certificate' => $certificate_file,
  65. 'private_key' => $private_key_file,
  66. 'password' => $private_key_password
  67. );
  68. }
  69.  
  70.  
  71. /**
  72. * Takes a date, removes comments and cleans up some common formatting inconsistencies
  73. *
  74. * @param string $date The date to clean
  75. * @return string The cleaned date
  76. */
  77. static private function cleanDate($date)
  78. {
  79. $date = preg_replace('#\([^)]+\)#', ' ', trim($date));
  80. $date = preg_replace('#\s+#', ' ', $date);
  81. $date = preg_replace('#(\d+)-([a-z]+)-(\d{4})#i', '\1 \2 \3', $date);
  82. $date = preg_replace('#^[a-z]+\s*,\s*#i', '', trim($date));
  83. return trim($date);
  84. }
  85.  
  86.  
  87. /**
  88. * Decodes encoded-word headers of any encoding into raw UTF-8
  89. *
  90. * @param string $text The header value to decode
  91. * @return string The decoded UTF-8
  92. */
  93. static private function decodeHeader($text)
  94. {
  95. $parts = preg_split('#(=\?[^\?]+\?[QB]\?[^\?]+\?=)#i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
  96.  
  97. $part_with_encoding = array();
  98. $output = '';
  99. foreach ($parts as $part) {
  100. if ($part === '') {
  101. continue;
  102. }
  103.  
  104. if (preg_match_all('#=\?([^\?]+)\?([QB])\?([^\?]+)\?=#i', $part, $matches, PREG_SET_ORDER)) {
  105. foreach ($matches as $match) {
  106. if (strtoupper($match[2]) == 'Q') {
  107. $part_string = rawurldecode(strtr(
  108. $match[3],
  109. array(
  110. '=' => '%',
  111. '_' => ' '
  112. )
  113. ));
  114. } else {
  115. $part_string = base64_decode($match[3]);
  116. }
  117. $lower_encoding = strtolower($match[1]);
  118. $last_key = count($part_with_encoding) - 1;
  119. if (isset($part_with_encoding[$last_key]) && $part_with_encoding[$last_key]['encoding'] == $lower_encoding) {
  120. $part_with_encoding[$last_key]['string'] .= $part_string;
  121. } else {
  122. $part_with_encoding[] = array('encoding' => $lower_encoding, 'string' => $part_string);
  123. }
  124. }
  125.  
  126. } else {
  127. $last_key = count($part_with_encoding) - 1;
  128. if (isset($part_with_encoding[$last_key]) && $part_with_encoding[$last_key]['encoding'] == 'iso-8859-1') {
  129. $part_with_encoding[$last_key]['string'] .= $part;
  130. } else {
  131. $part_with_encoding[] = array('encoding' => 'iso-8859-1', 'string' => $part);
  132. }
  133. }
  134. }
  135.  
  136. foreach ($part_with_encoding as $part) {
  137. $output .= self::iconv($part['encoding'], 'UTF-8', $part['string']);
  138. }
  139.  
  140. return $output;
  141. }
  142.  
  143.  
  144. /**
  145. * Handles an individual part of a multipart message
  146. *
  147. * @param array $info An array of information about the message
  148. * @param array $structure An array describing the structure of the message
  149. * @return array The modified $info array
  150. */
  151. static private function handlePart($info, $structure)
  152. {
  153. if ($structure['type'] == 'multipart') {
  154. foreach ($structure['parts'] as $part) {
  155. $info = self::handlePart($info, $part);
  156. }
  157. return $info;
  158. }
  159.  
  160. if ($structure['type'] == 'application' && in_array($structure['subtype'], array('pkcs7-mime', 'x-pkcs7-mime'))) {
  161. $to = NULL;
  162. if (isset($info['headers']['to'][0])) {
  163. $to = $info['headers']['to'][0]['mailbox'];
  164. if (!empty($info['headers']['to'][0]['host'])) {
  165. $to .= '@' . $info['headers']['to'][0]['host'];
  166. }
  167. }
  168. if ($to && !empty(self::$smime_pairs[$to]['private_key'])) {
  169. if (self::handleSMIMEDecryption($info, $structure, self::$smime_pairs[$to])) {
  170. return $info;
  171. }
  172. }
  173. }
  174.  
  175. if ($structure['type'] == 'application' && in_array($structure['subtype'], array('pkcs7-signature', 'x-pkcs7-signature'))) {
  176. $from = NULL;
  177. if (isset($info['headers']['from'])) {
  178. $from = $info['headers']['from']['mailbox'];
  179. if (!empty($info['headers']['from']['host'])) {
  180. $from .= '@' . $info['headers']['from']['host'];
  181. }
  182. }
  183. if ($from && !empty(self::$smime_pairs[$from]['certificate'])) {
  184. if (self::handleSMIMEVerification($info, $structure, self::$smime_pairs[$from])) {
  185. return $info;
  186. }
  187. }
  188. }
  189.  
  190. $data = $structure['data'];
  191.  
  192. if ($structure['encoding'] == 'base64') {
  193. $content = '';
  194. foreach (explode("\r\n", $data) as $line) {
  195. $content .= base64_decode($line);
  196. }
  197. } elseif ($structure['encoding'] == 'quoted-printable') {
  198. $content = quoted_printable_decode($data);
  199. } else {
  200. $content = $data;
  201. }
  202.  
  203. if ($structure['type'] == 'text') {
  204. $charset = 'iso-8859-1';
  205. foreach ($structure['type_fields'] as $field => $value) {
  206. if (strtolower($field) == 'charset') {
  207. $charset = $value;
  208. break;
  209. }
  210. }
  211. $content = self::iconv($charset, 'UTF-8', $content);
  212. if ($structure['subtype'] == 'html') {
  213. $content = preg_replace('#(content=(["\'])text/html\s*;\s*charset=(["\']?))' . preg_quote($charset, '#') . '(\3\2)#i', '\1utf-8\4', $content);
  214. }
  215. }
  216.  
  217. // This indicates a content-id which is used for multipart/related
  218. if ($structure['content_id']) {
  219. if (!isset($info['related'])) {
  220. $info['related'] = array();
  221. }
  222. $cid = $structure['content_id'][0] == '<' ? substr($structure['content_id'], 1, -1) : $structure['content_id'];
  223. if(isset($structure['type_fields']['name'])){
  224. $filename=$structure['type_fields']['name'];
  225. }else{
  226. $filename=$cid;
  227. }
  228. $info['related']['cid:' . $cid] = array(
  229. 'mimetype' => $structure['type'] . '/' . $structure['subtype'],
  230. 'data' => $content,
  231. 'filename' => $filename
  232. );
  233. return $info;
  234. }
  235.  
  236.  
  237. $has_disposition = !empty($structure['disposition']);
  238. $is_text = $structure['type'] == 'text' && $structure['subtype'] == 'plain';
  239. $is_html = $structure['type'] == 'text' && $structure['subtype'] == 'html';
  240.  
  241. // If the part doesn't have a disposition and is not the default text or html, set the disposition to inline
  242. if (!$has_disposition && ((!$is_text || !empty($info['text'])) && (!$is_html || !empty($info['html'])))) {
  243. $is_web_image = $structure['type'] == 'image' && in_array($structure['subtype'], array('gif', 'png', 'jpeg', 'pjpeg'));
  244. $structure['disposition'] = $is_text || $is_html || $is_web_image ? 'inline' : 'attachment';
  245. $structure['disposition_fields'] = array();
  246. $has_disposition = TRUE;
  247. }
  248.  
  249.  
  250. // Attachments or inline content
  251. if ($has_disposition) {
  252.  
  253. $filename = '';
  254. foreach ($structure['disposition_fields'] as $field => $value) {
  255. if (strtolower($field) == 'filename') {
  256. $filename = $value;
  257. break;
  258. }
  259. }
  260. foreach ($structure['type_fields'] as $field => $value) {
  261. if (strtolower($field) == 'name') {
  262. $filename = $value;
  263. break;
  264. }
  265. }
  266.  
  267. // This automatically handles primary content that has a content-disposition header on it
  268. if ($structure['disposition'] == 'inline' && $filename === '') {
  269. if ($is_text && !isset($info['text'])) {
  270. $info['text'] = $content;
  271. return $info;
  272. }
  273. if ($is_html && !isset($info['html'])) {
  274. $info['html'] = $content;
  275. return $info;
  276. }
  277. }
  278.  
  279. if (!isset($info[$structure['disposition']])) {
  280. $info[$structure['disposition']] = array();
  281. }
  282.  
  283. $info[$structure['disposition']][] = array(
  284. 'filename' => $filename,
  285. 'mimetype' => $structure['type'] . '/' . $structure['subtype'],
  286. 'data' => $content
  287. );
  288. return $info;
  289. }
  290.  
  291. if ($is_text) {
  292. $info['text'] = $content;
  293. return $info;
  294. }
  295.  
  296. if ($is_html) {
  297. $info['html'] = $content;
  298. return $info;
  299. }
  300. }
  301.  
  302.  
  303. /**
  304. * Tries to decrypt an S/MIME message using a private key
  305. *
  306. * @param array &$info The array of information about a message
  307. * @param array $structure The structure of this part
  308. * @param array $smime_pair An associative array containing an S/MIME certificate, private key and password
  309. * @return boolean If the message was decrypted
  310. */
  311. static private function handleSMIMEDecryption(&$info, $structure, $smime_pair)
  312. {
  313. $plaintext_file = tempnam('', '__fMailbox_');
  314. $ciphertext_file = tempnam('', '__fMailbox_');
  315.  
  316. $headers = array();
  317. $headers[] = "Content-Type: " . $structure['type'] . '/' . $structure['subtype'];
  318. $headers[] = "Content-Transfer-Encoding: " . $structure['encoding'];
  319. $header = "Content-Disposition: " . $structure['disposition'];
  320. foreach ($structure['disposition_fields'] as $field => $value) {
  321. $header .= '; ' . $field . '="' . $value . '"';
  322. }
  323. $headers[] = $header;
  324.  
  325. file_put_contents($ciphertext_file, join("\r\n", $headers) . "\r\n\r\n" . $structure['data']);
  326.  
  327. $private_key = openssl_pkey_get_private(
  328. $smime_pair['private_key']->read(),
  329. $smime_pair['password']
  330. );
  331. $certificate = $smime_pair['certificate']->read();
  332.  
  333. $result = openssl_pkcs7_decrypt($ciphertext_file, $plaintext_file, $certificate, $private_key);
  334. unlink($ciphertext_file);
  335.  
  336. if (!$result) {
  337. unlink($plaintext_file);
  338. return FALSE;
  339. }
  340.  
  341. $contents = file_get_contents($plaintext_file);
  342. $info['raw_message'] = $contents;
  343. $info = self::handlePart($info, self::parseStructure($contents));
  344. $info['decrypted'] = TRUE;
  345.  
  346. unlink($plaintext_file);
  347. return TRUE;
  348. }
  349.  
  350.  
  351.  
  352. /**
  353. * Takes a message with an S/MIME signature and verifies it if possible
  354. *
  355. * @param array &$info The array of information about a message
  356. * @param array $structure
  357. * @param array $smime_pair An associative array containing an S/MIME certificate file
  358. * @return boolean If the message was verified
  359. */
  360. static private function handleSMIMEVerification(&$info, $structure, $smime_pair)
  361. {
  362. $certificates_file = tempnam('', '__fMailbox_');
  363. $ciphertext_file = tempnam('', '__fMailbox_');
  364.  
  365. file_put_contents($ciphertext_file, $info['raw_message']);
  366.  
  367. $result = openssl_pkcs7_verify(
  368. $ciphertext_file,
  369. PKCS7_NOINTERN | PKCS7_NOVERIFY,
  370. $certificates_file,
  371. array(),
  372. $smime_pair['certificate']->getPath()
  373. );
  374. unlink($ciphertext_file);
  375. unlink($certificates_file);
  376.  
  377. if (!$result || $result === -1) {
  378. return FALSE;
  379. }
  380.  
  381. $info['verified'] = TRUE;
  382.  
  383. return TRUE;
  384. }
  385.  
  386.  
  387. /**
  388. * This works around a bug in MAMP 1.9.4+ and PHP 5.3 where iconv()
  389. * does not seem to properly assign the return value to a variable, but
  390. * does work when returning the value.
  391. *
  392. * @param string $in_charset The incoming character encoding
  393. * @param string $out_charset The outgoing character encoding
  394. * @param string $string The string to convert
  395. * @return string The converted string
  396. */
  397. static private function iconv($in_charset, $out_charset, $string)
  398. {
  399. return iconv($in_charset, $out_charset, $string);
  400. }
  401.  
  402.  
  403. /**
  404. * Joins parsed emails into a comma-delimited string
  405. *
  406. * @param array $emails An array of emails split into personal, mailbox and host parts
  407. * @return string An comma-delimited list of emails
  408. */
  409. static private function joinEmails($emails)
  410. {
  411. $output = '';
  412. foreach ($emails as $email) {
  413. if ($output) { $output .= ', '; }
  414.  
  415. if (!isset($email[0])) {
  416. $email[0] = !empty($email['personal']) ? $email['personal'] : '';
  417. $email[2] = $email['mailbox'];
  418. $email[3] = !empty($email['host']) ? $email['host'] : '';
  419. }
  420.  
  421. $address = $email[2];
  422. if (!empty($email[3])) {
  423. $address .= '@' . $email[3];
  424. }
  425. $output .= fEmail::combineNameEmail($email[0], $address);
  426. }
  427. return $output;
  428. }
  429.  
  430.  
  431. /**
  432. * Parses a string representation of an email into the persona, mailbox and host parts
  433. *
  434. * @param string $string The email string to parse
  435. * @return array An associative array with the key `mailbox`, and possibly `host` and `personal`
  436. */
  437. static private function parseEmail($string)
  438. {
  439. $string=stripslashes($string);
  440. $email_regex = '((?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")(?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*)@((?:[a-z0-9\\-]+\.)+[a-z]{2,}|\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\])';
  441.  
  442. $name_regex = '((?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*)(?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*)';
  443.  
  444. if (preg_match('~^[ \t]*' . $name_regex . '[ \t]*<[ \t]*' . $email_regex . '[ \t]*>[ \t]*$~ixD', $string, $match)) {
  445. $match[1] = trim($match[1]);
  446. if ($match[1][0] == '"' && substr($match[1], -1) == '"') {
  447. $match[1] = substr($match[1], 1, -1);
  448. }
  449. return array(
  450. 'personal' => self::decodeHeader($match[1]),
  451. 'mailbox' => self::decodeHeader($match[2]),
  452. 'host' => self::decodeHeader($match[3])
  453. );
  454.  
  455. } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . '(?:[ \t]*>)?[ \t]*$~ixD', $string, $match)) {
  456. return array(
  457. 'mailbox' => self::decodeHeader($match[1]),
  458. 'host' => self::decodeHeader($match[2])
  459. );
  460.  
  461. // This handles the outdated practice of including the personal
  462. // part of the email in a comment after the email address
  463. } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . '(?:[ \t]*>)?[ \t]*\(([^)]+)\)[ \t]*$~ixD', $string, $match)) {
  464. $match[3] = trim($match[1]);
  465. if ($match[3][0] == '"' && substr($match[3], -1) == '"') {
  466. $match[3] = substr($match[3], 1, -1);
  467. }
  468.  
  469. return array(
  470. 'personal' => self::decodeHeader($match[3]),
  471. 'mailbox' => self::decodeHeader($match[1]),
  472. 'host' => self::decodeHeader($match[2])
  473. );
  474. }
  475.  
  476. if (strpos($string, '@') !== FALSE) {
  477. list ($mailbox, $host) = explode('@', $string, 2);
  478. return array(
  479. 'mailbox' => self::decodeHeader($mailbox),
  480. 'host' => self::decodeHeader($host)
  481. );
  482. }
  483.  
  484. return array(
  485. 'mailbox' => self::decodeHeader($string),
  486. 'host' => ''
  487. );
  488. }
  489.  
  490.  
  491. /**
  492. * Parses full email headers into an associative array
  493. *
  494. * @param string $headers The header to parse
  495. * @param string $filter Remove any headers that match this
  496. * @return array The parsed headers
  497. */
  498. static private function parseHeaders($headers, $filter=NULL)
  499. {
  500. $header_lines = preg_split("#\r\n(?!\s)#", trim($headers));
  501. $single_email_fields = array('from', 'sender', 'reply-to');
  502. $multi_email_fields = array('to', 'cc');
  503. $additional_info_fields = array('content-type', 'content-disposition');
  504.  
  505. $headers = array();
  506. foreach ($header_lines as $header_line) {
  507. $header_line = preg_replace("#\r\n\s+#", '', $header_line);
  508.  
  509. list ($header, $value) = preg_split('#:\s*#', $header_line, 2);
  510. $header = strtolower($header);
  511.  
  512. if (strpos($header, $filter) !== FALSE) {
  513. continue;
  514. }
  515.  
  516. $is_single_email = in_array($header, $single_email_fields);
  517. $is_multi_email = in_array($header, $multi_email_fields);
  518. $is_additional_info_field = in_array($header, $additional_info_fields);
  519.  
  520. if ($is_additional_info_field) {
  521. $pieces = preg_split('#;\s*#', $value, 2);
  522. $value = $pieces[0];
  523.  
  524. $headers[$header] = array('value' => self::decodeHeader($value));
  525.  
  526. $fields = array();
  527. if (!empty($pieces[1])) {
  528. preg_match_all('#(\w+)=("([^"]+)"|([^\s;]+))(?=;|$)#', $pieces[1], $matches, PREG_SET_ORDER);
  529. foreach ($matches as $match) {
  530. $fields[$match[1]] = self::decodeHeader(!empty($match[4]) ? $match[4] : $match[3]);
  531. }
  532. }
  533. $headers[$header]['fields'] = $fields;
  534.  
  535. } elseif ($is_single_email) {
  536. $headers[$header] = self::parseEmail($value);
  537.  
  538. } elseif ($is_multi_email) {
  539. $strings = array();
  540.  
  541. preg_match_all('#"[^"]+?"#', $value, $matches, PREG_SET_ORDER);
  542. foreach ($matches as $i => $match) {
  543. $strings[] = $match[0];
  544. $value = preg_replace('#' . preg_quote($match[0], '#') . '#', ':string' . sizeof($strings), $value, 1);
  545. }
  546. preg_match_all('#\([^)]+?\)#', $value, $matches, PREG_SET_ORDER);
  547. foreach ($matches as $i => $match) {
  548. $strings[] = $match[0];
  549. $value = preg_replace('#' . preg_quote($match[0], '#') . '#', ':string' . sizeof($strings), $value, 1);
  550. }
  551.  
  552. $emails = explode(',', $value);
  553. array_map('trim', $emails);
  554. foreach ($strings as $i => $string) {
  555. $emails = preg_replace(
  556. '#:string' . ($i+1) . '\b#',
  557. strtr($string, array('\\' => '\\\\', '$' => '\\$')),
  558. $emails,
  559. 1
  560. );
  561. }
  562.  
  563. $headers[$header] = array();
  564. foreach ($emails as $email) {
  565. $headers[$header][] = self::parseEmail($email);
  566. }
  567.  
  568. } elseif ($header == 'references') {
  569. $headers[$header] = array_map(array('fMailbox', 'decodeHeader'), preg_split('#(?<=>)\s+(?=<)#', $value));
  570.  
  571. } elseif ($header == 'received') {
  572. if (!isset($headers[$header])) {
  573. $headers[$header] = array();
  574. }
  575. $headers[$header][] = preg_replace('#\s+#', ' ', self::decodeHeader($value));
  576.  
  577. } else {
  578. $headers[$header] = self::decodeHeader($value);
  579. }
  580. }
  581.  
  582. return $headers;
  583. }
  584.  
  585.  
  586. /**
  587. * Parses a MIME message into an associative array of information
  588. *
  589. * The output includes the following keys:
  590. *
  591. * - `'received'`: The date the message was received by the server
  592. * - `'headers'`: An associative array of mail headers, the keys are the header names, in lowercase
  593. *
  594. * And one or more of the following:
  595. *
  596. * - `'text'`: The plaintext body
  597. * - `'html'`: The HTML body
  598. * - `'attachment'`: An array of attachments, each containing:
  599. * - `'filename'`: The name of the file
  600. * - `'mimetype'`: The mimetype of the file
  601. * - `'data'`: The raw contents of the file
  602. * - `'inline'`: An array of inline files, each containing:
  603. * - `'filename'`: The name of the file
  604. * - `'mimetype'`: The mimetype of the file
  605. * - `'data'`: The raw contents of the file
  606. * - `'related'`: An associative array of related files, such as embedded images, with the key `'cid:{content-id}'` and an array value containing:
  607. * - `'mimetype'`: The mimetype of the file
  608. * - `'data'`: The raw contents of the file
  609. * - `'verified'`: If the message contents were verified via an S/MIME certificate - if not verified the smime.p7s will be listed as an attachment
  610. * - `'decrypted'`: If the message contents were decrypted via an S/MIME private key - if not decrypted the smime.p7m will be listed as an attachment
  611. *
  612. * All values in `headers`, `text` and `body` will have been decoded to
  613. * UTF-8. Files in the `attachment`, `inline` and `related` array will all
  614. * retain their original encodings.
  615. *
  616. * @param string $message The full source of the email message
  617. * @param boolean $convert_newlines If `\r\n` should be converted to `\n` in the `text` and `html` parts the message
  618. * @return array The parsed email message - see method description for details
  619. */
  620. static public function parseMessage($message, $convert_newlines=FALSE)
  621. {
  622. $info = array();
  623. list ($headers, $body) = explode("\r\n\r\n", $message, 2);
  624. $parsed_headers = self::parseHeaders($headers);
  625. $info['received'] = self::cleanDate(preg_replace('#^.*;\s*([^;]+)$#', '\1', $parsed_headers['received'][0]));
  626. $info['headers'] = array();
  627. foreach ($parsed_headers as $header => $value) {
  628. if (substr($header, 0, 8) == 'content-') {
  629. continue;
  630. }
  631. $info['headers'][$header] = $value;
  632. }
  633. //print_r($info['headers']); --> shows only related .. of course I guess ?
  634. $info['raw_headers'] = $headers;
  635. $info['raw_message'] = $message;
  636. //print_r(self::parseStructure($body, $parsed_headers));
  637. $info = self::handlePart($info, self::parseStructure($body, $parsed_headers));
  638. unset($info['raw_message']);
  639. unset($info['raw_headers']);
  640.  
  641. if ($convert_newlines) {
  642. if (isset($info['text'])) {
  643. $info['text'] = str_replace("\r\n", "\n", $info['text']);
  644. }
  645. if (isset($info['html'])) {
  646. $info['html'] = str_replace("\r\n", "\n", $info['html']);
  647. }
  648. }
  649.  
  650. if (isset($info['text'])) {
  651. $info['text'] = preg_replace('#\r?\n$#D', '', $info['text']);
  652. }else{
  653. $info['text'] = "";
  654. }
  655. if (isset($info['html'])) {
  656. $info['html'] = preg_replace('#\r?\n$#D', '', $info['html']);
  657. }else{
  658. $info['html'] = "";
  659. }
  660.  
  661. return $info;
  662. }
  663.  
  664.  
  665. /**
  666. * Takes a response from an IMAP command and parses it into a
  667. * multi-dimensional array
  668. *
  669. * @param string $text The IMAP command response
  670. * @param boolean $top_level If we are parsing the top level
  671. * @return array The parsed representation of the response text
  672. */
  673. static private function parseResponse($text, $top_level=FALSE)
  674. {
  675. $regex = '[\\\\\w.\[\]]+|"([^"\\\\]+|\\\\"|\\\\\\\\)*"|\((?:(?1)[ \t]*)*\)';
  676.  
  677. if (preg_match('#\{(\d+)\}#', $text, $match)) {
  678. $regex = '\{' . $match[1] . '\}\r\n.{' . ($match[1]) . '}|' . $regex;
  679. }
  680.  
  681. preg_match_all('#(' . $regex . ')#s', $text, $matches, PREG_SET_ORDER);
  682. $output = array();
  683. foreach ($matches as $match) {
  684. if (substr($match[0], 0, 1) == '"') {
  685. $output[] = str_replace('\\"', '"', substr($match[0], 1, -1));
  686. } elseif (substr($match[0], 0, 1) == '(') {
  687. $output[] = self::parseResponse(substr($match[0], 1, -1));
  688. } elseif (substr($match[0], 0, 1) == '{') {
  689. $output[] = preg_replace('#^[^\r]+\r\n#', '', $match[0]);
  690. } else {
  691. $output[] = $match[0];
  692. }
  693. }
  694.  
  695. if ($top_level) {
  696. $new_output = array();
  697. $total_size = count($output);
  698. for ($i = 0; $i < $total_size; $i = $i + 2) {
  699. $new_output[strtolower($output[$i])] = $output[$i+1];
  700. }
  701. $output = $new_output;
  702. }
  703.  
  704. return $output;
  705. }
  706.  
  707.  
  708. /**
  709. * Takes the raw contents of a MIME message and creates an array that
  710. * describes the structure of the message
  711. *
  712. * @param string $data The contents to get the structure of
  713. * @param string $headers The parsed headers for the message - if not present they will be extracted from the `$data`
  714. * @return array The multi-dimensional, associative array containing the message structure
  715. */
  716. static private function parseStructure($data, $headers=NULL)
  717. {
  718. if (!$headers) {
  719. list ($headers, $data) = explode("\r\n\r\n", $data, 2);
  720. $headers = self::parseHeaders($headers);
  721. }
  722.  
  723. if (!isset($headers['content-type'])) {
  724. $headers['content-type'] = array(
  725. 'value' => 'text/plain',
  726. 'fields' => array()
  727. );
  728. }
  729.  
  730. list ($type, $subtype) = explode('/', strtolower($headers['content-type']['value']), 2);
  731.  
  732. if ($type == 'multipart') {
  733. $structure = array(
  734. 'type' => $type,
  735. 'subtype' => $subtype,
  736. 'parts' => array()
  737. );
  738. if(array_key_exists('boundary',$headers['content-type']['fields'])){
  739. $boundary = $headers['content-type']['fields']['boundary'];
  740. }elseif(array_key_exists('Boundary',$headers['content-type']['fields'])){
  741. $boundary = $headers['content-type']['fields']['Boundary'];
  742. }
  743. $start_pos = strpos($data, '--' . $boundary) + strlen($boundary) + 4;
  744. $end_pos = strrpos($data, '--' . $boundary . '--') - 2;
  745. $sub_contents = explode("\r\n--" . $boundary . "\r\n", substr(
  746. $data,
  747. $start_pos,
  748. $end_pos - $start_pos
  749. ));
  750. foreach ($sub_contents as $sub_content) {
  751. $structure['parts'][] = self::parseStructure($sub_content);
  752. }
  753.  
  754. } else {
  755. $structure = array(
  756. 'type' => $type,
  757. 'type_fields' => !empty($headers['content-type']['fields']) ? $headers['content-type']['fields'] : array(),
  758. 'subtype' => $subtype,
  759. 'content_id' => isset($headers['content-id']) ? $headers['content-id'] : NULL,
  760. 'encoding' => isset($headers['content-transfer-encoding']) ? strtolower($headers['content-transfer-encoding']) : '8bit',
  761. 'disposition' => isset($headers['content-disposition']) ? strtolower($headers['content-disposition']['value']) : NULL,
  762. 'disposition_fields' => isset($headers['content-disposition']) ? $headers['content-disposition']['fields'] : array(),
  763. 'data' => $data
  764. );
  765. }
  766.  
  767. return $structure;
  768. }
  769.  
  770.  
  771. /**
  772. * Resets the configuration of the class
  773. *
  774. * @internal
  775. *
  776. * @return void
  777. */
  778. static public function reset()
  779. {
  780. self::$smime_pairs = array();
  781. }
  782.  
  783.  
  784. /**
  785. * Takes an associative array and unfolds the keys and values so that the
  786. * result in an integer-indexed array of `0 => key1, 1 => value1, 2 => key2,
  787. * 3 => value2, ...`.
  788. *
  789. * @param array $array The array to unfold
  790. * @return array The unfolded array
  791. */
  792. static private function unfoldAssociativeArray($array)
  793. {
  794. $new_array = array();
  795. foreach ($array as $key => $value) {
  796. $new_array[] = $key;
  797. $new_array[] = $value;
  798. }
  799. return $new_array;
  800. }
  801.  
  802.  
  803. /**
  804. * A counter to use for generating command keys
  805. *
  806. * @var integer
  807. */
  808. private $command_num = 1;
  809.  
  810. /**
  811. * The connection resource
  812. *
  813. * @var resource
  814. */
  815. private $connection;
  816.  
  817. /**
  818. * If debugging has been enabled
  819. *
  820. * @var boolean
  821. */
  822. private $debug;
  823.  
  824. /**
  825. * The server hostname or IP address
  826. *
  827. * @var string
  828. */
  829. private $host;
  830.  
  831. /**
  832. * The password for the account
  833. *
  834. * @var string
  835. */
  836. private $password;
  837.  
  838. /**
  839. * The port for the server
  840. *
  841. * @var integer
  842. */
  843. private $port;
  844.  
  845. /**
  846. * If the connection to the server should be secure
  847. *
  848. * @var boolean
  849. */
  850. private $secure;
  851.  
  852. /**
  853. * The timeout for the connection
  854. *
  855. * @var integer
  856. */
  857. private $timeout = 5;
  858.  
  859. /**
  860. * The type of mailbox, `'imap'` or `'pop3'`
  861. *
  862. * @var string
  863. */
  864. private $type;
  865.  
  866. /**
  867. * The username for the account
  868. *
  869. * @var string
  870. */
  871. private $username;
  872.  
  873.  
  874. /**
  875. * Configures the connection to the server
  876. *
  877. * Please note that the GMail POP3 server does not act like other POP3
  878. * servers and the GMail IMAP server should be used instead. GMail POP3 only
  879. * allows retrieving a message once - during future connections the email
  880. * in question will no longer be available.
  881. *
  882. * @param string $type The type of mailbox, `'pop3'` or `'imap'`
  883. * @param string $host The server hostname or IP address
  884. * @param string $username The user to log in as
  885. * @param string $password The user's password
  886. * @param integer $port The port to connect via - only required if non-standard
  887. * @param boolean $secure If SSL should be used for the connection - this requires the `openssl` extension
  888. * @param integer $timeout The timeout to use when connecting
  889. * @return fMailbox
  890. */
  891. public function __construct($type, $host, $username, $password, $port=NULL, $secure=FALSE, $timeout=NULL)
  892. {
  893. if ($timeout === NULL) {
  894. $timeout = ini_get('default_socket_timeout');
  895. }
  896.  
  897. $valid_types = array('imap', 'pop3');
  898. if (!in_array($type, $valid_types)) {
  899. throw new fProgrammerException(
  900. 'The mailbox type specified, %1$s, in invalid. Must be one of: %2$s.',
  901. $type,
  902. join(', ', $valid_types)
  903. );
  904. }
  905.  
  906. if ($port === NULL) {
  907. if ($type == 'imap') {
  908. $port = !$secure ? 143 : 993;
  909. } else {
  910. $port = !$secure ? 110 : 995;
  911. }
  912. }
  913.  
  914. if ($secure && !extension_loaded('openssl')) {
  915. throw new fEnvironmentException(
  916. 'A secure connection was requested, but the %s extension is not installed',
  917. 'openssl'
  918. );
  919. }
  920.  
  921. $this->type = $type;
  922. $this->host = $host;
  923. $this->username = $username;
  924. $this->password = $password;
  925. $this->port = $port;
  926. $this->secure = $secure;
  927. $this->timeout = $timeout;
  928. }
  929.  
  930.  
  931. /**
  932. * Disconnects from the server
  933. *
  934. * @return void
  935. */
  936. public function __destruct()
  937. {
  938. $this->close();
  939. }
  940.  
  941.  
  942. /**
  943. * Closes the connection to the server
  944. *
  945. * @return void
  946. */
  947. public function close()
  948. {
  949. if (!$this->connection) {
  950. return;
  951. }
  952.  
  953. if ($this->type == 'imap') {
  954. $this->write('LOGOUT');
  955. } else {
  956. $this->write('QUIT', 1);
  957. }
  958.  
  959. $this->connection = NULL;
  960. }
  961.  
  962.  
  963. /**
  964. * Connects to the server
  965. *
  966. * @return void
  967. */
  968. private function connect()
  969. {
  970. if ($this->connection) {
  971. return;
  972. }
  973.  
  974. fCore::startErrorCapture(E_WARNING);
  975.  
  976. $this->connection = fsockopen(
  977. $this->secure ? 'tls://' . $this->host : $this->host,
  978. $this->port,
  979. $error_number,
  980. $error_string,
  981. $this->timeout
  982. );
  983.  
  984. foreach (fCore::stopErrorCapture('#ssl#i') as $error) {
  985. throw new fConnectivityException('There was an error connecting to the server. A secure connection was requested, but was not available. Try a non-secure connection instead.');
  986. }
  987.  
  988. if (!$this->connection) {
  989. throw new fConnectivityException('There was an error connecting to the server');
  990. }
  991.  
  992. stream_set_timeout($this->connection, $this->timeout);
  993.  
  994.  
  995. if ($this->type == 'imap') {
  996. if (!$this->secure && extension_loaded('openssl')) {
  997. $response = $this->write('CAPABILITY');
  998. if (preg_match('#\bstarttls\b#i', $response[0])) {
  999. $this->write('STARTTLS');
  1000. do {
  1001. if (isset($res)) {
  1002. sleep(0.1);
  1003. }
  1004. $res = stream_socket_enable_crypto($this->connection, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  1005. } while ($res === 0);
  1006. }
  1007. }
  1008.  
  1009. $response = $this->write('LOGIN ' . $this->username . ' ' . $this->password);
  1010. if (!$response || !preg_match('#^[^ ]+\s+OK#', $response[count($response)-1])) {
  1011. throw new fValidationException(
  1012. 'The username and password provided were not accepted for the %1$s server %2$s on port %3$s',
  1013. strtoupper($this->type),
  1014. $this->host,
  1015. $this->port
  1016. );
  1017. }
  1018. $this->write('SELECT "INBOX"');
  1019.  
  1020. } elseif ($this->type == 'pop3') {
  1021. $response = $this->read(1);
  1022. if (isset($response[0])) {
  1023. if ($response[0][0] == '-') {
  1024. throw new fConnectivityException(
  1025. 'There was an error connecting to the POP3 server %1$s on port %2$s',
  1026. $this->host,
  1027. $this->port
  1028. );
  1029. }
  1030. preg_match('#<[^@]+@[^>]+>#', $response[0], $match);
  1031. }
  1032.  
  1033. if (!$this->secure && extension_loaded('openssl')) {
  1034. $response = $this->write('STLS', 1);
  1035. if ($response[0][0] == '+') {
  1036. do {
  1037. if (isset($res)) {
  1038. sleep(0.1);
  1039. }
  1040. $res = stream_socket_enable_crypto($this->connection, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  1041. } while ($res === 0);
  1042. if ($res === FALSE) {
  1043. throw new fConnectivityException('Error establishing secure connection');
  1044. }
  1045. }
  1046. }
  1047.  
  1048. $authenticated = FALSE;
  1049. if (isset($match[0])) {
  1050. $response = $this->write('APOP ' . $this->username . ' ' . md5($match[0] . $this->password), 1);
  1051. if (isset($response[0]) && $response[0][0] == '+') {
  1052. $authenticated = TRUE;
  1053. }
  1054. }
  1055.  
  1056. if (!$authenticated) {
  1057. $response = $this->write('USER ' . $this->username, 1);
  1058. if ($response[0][0] == '+') {
  1059. $response = $this->write('PASS ' . $this->password, 1);
  1060. if (isset($response[0][0]) && $response[0][0] == '+') {
  1061. $authenticated = TRUE;
  1062. }
  1063. }
  1064. }
  1065.  
  1066. if (!$authenticated) {
  1067. throw new fValidationException(
  1068. 'The username and password provided were not accepted for the %1$s server %2$s on port %3$s',
  1069. strtoupper($this->type),
  1070. $this->host,
  1071. $this->port
  1072. );
  1073. }
  1074. }
  1075. }
  1076.  
  1077.  
  1078. /**
  1079. * Deletes one or more messages from the server
  1080. *
  1081. * Passing more than one UID at a time is more efficient for IMAP mailboxes,
  1082. * whereas POP3 mailboxes will see no difference in performance.
  1083. *
  1084. * @param integer|array $uid The UID(s) of the message(s) to delete
  1085. * @return void
  1086. */
  1087. public function deleteMessages($uid)
  1088. {
  1089. $this->connect();
  1090.  
  1091. settype($uid, 'array');
  1092.  
  1093. if ($this->type == 'imap') {
  1094. $this->write('UID STORE ' . join(',', $uid) . ' +FLAGS (\\Deleted)');
  1095. $this->write('EXPUNGE');
  1096.  
  1097. } elseif ($this->type == 'pop3') {
  1098. foreach ($uid as $id) {
  1099. $this->write('DELE ' . $id, 1);
  1100. }
  1101. }
  1102. }
  1103.  
  1104.  
  1105.  
  1106. /**
  1107. * markAsUnreads one or more messages from the server
  1108. *
  1109. * Passing more than one UID at a time is more efficient for IMAP mailboxes,
  1110. * whereas POP3 mailboxes will see no difference in performance.
  1111. *
  1112. * @param integer|array $uid The UID(s) of the message(s) to markAsUnread
  1113. * @return void
  1114. */
  1115. public function markAsUnreadMessages($uid)
  1116. {
  1117. $this->connect();
  1118.  
  1119. settype($uid, 'array');
  1120.  
  1121. if ($this->type == 'imap') {
  1122. $this->write('UID STORE ' . join(',', $uid) . ' -FLAGS (\\Seen)');
  1123. }
  1124. }
  1125.  
  1126.  
  1127. /**
  1128. * Sets if debug messages should be shown
  1129. *
  1130. * @param boolean $flag If debugging messages should be shown
  1131. * @return void
  1132. */
  1133. public function enableDebugging($flag)
  1134. {
  1135. $this->debug = (boolean) $flag;
  1136. }
  1137.  
  1138.  
  1139. /**
  1140. * Retrieves a single message from the server
  1141. *
  1142. * The output includes the following keys:
  1143. *
  1144. * - `'uid'`: The UID of the message
  1145. * - `'received'`: The date the message was received by the server
  1146. * - `'headers'`: An associative array of mail headers, the keys are the header names, in lowercase
  1147. *
  1148. * And one or more of the following:
  1149. *
  1150. * - `'text'`: The plaintext body
  1151. * - `'html'`: The HTML body
  1152. * - `'attachment'`: An array of attachments, each containing:
  1153. * - `'filename'`: The name of the file
  1154. * - `'mimetype'`: The mimetype of the file
  1155. * - `'data'`: The raw contents of the file
  1156. * - `'inline'`: An array of inline files, each containing:
  1157. * - `'filename'`: The name of the file
  1158. * - `'mimetype'`: The mimetype of the file
  1159. * - `'data'`: The raw contents of the file
  1160. * - `'related'`: An associative array of related files, such as embedded images, with the key `'cid:{content-id}'` and an array value containing:
  1161. * - `'mimetype'`: The mimetype of the file
  1162. * - `'data'`: The raw contents of the file
  1163. * - `'verified'`: If the message contents were verified via an S/MIME certificate - if not verified the smime.p7s will be listed as an attachment
  1164. * - `'decrypted'`: If the message contents were decrypted via an S/MIME private key - if not decrypted the smime.p7m will be listed as an attachment
  1165. *
  1166. * All values in `headers`, `text` and `body` will have been decoded to
  1167. * UTF-8. Files in the `attachment`, `inline` and `related` array will all
  1168. * retain their original encodings.
  1169. *
  1170. * @param integer $uid The UID of the message to retrieve
  1171. * @param boolean $convert_newlines If `\r\n` should be converted to `\n` in the `text` and `html` parts the message
  1172. * @return array The parsed email message - see method description for details
  1173. */
  1174. public function fetchMessage($uid, $convert_newlines=FALSE)
  1175. {
  1176. $this->connect();
  1177.  
  1178. if ($this->type == 'imap') {
  1179. $response = $this->write('UID FETCH ' . $uid . ' (BODY[])');
  1180.  
  1181. preg_match('#\{(\d+)\}$#', $response[0], $match);
  1182.  
  1183. $message = '';
  1184. foreach ($response as $i => $line) {
  1185. if (!$i) { continue; }
  1186. if (strlen($message) + strlen($line) + 2 > $match[1]) {
  1187. $message .= substr($line . "\r\n", 0, $match[1] - strlen($message));
  1188. } else {
  1189. $message .= $line . "\r\n";
  1190. }
  1191. }
  1192.  
  1193. $info = self::parseMessage($message, $convert_newlines);
  1194. $info['uid'] = $uid;
  1195.  
  1196. } elseif ($this->type == 'pop3') {
  1197. $response = $this->write('RETR ' . $uid);
  1198. array_shift($response);
  1199. $response = join("\r\n", $response);
  1200.  
  1201. $info = self::parseMessage($response, $convert_newlines);
  1202. $info['uid'] = $uid;
  1203. }
  1204.  
  1205. return $info;
  1206. }
  1207.  
  1208. public function listUnseenMessages(){
  1209. $this->connect();
  1210. if ($this->type == 'imap') {
  1211. $output = array();
  1212. $unseen_list = $this->write('UID SEARCH UNSEEN');
  1213. // print_r($unseen_list);
  1214. if($unseen_list[1]=="a0003 OK SEARCH completed." || $unseen_list[1]=="a0003 OK UID completed"){
  1215. //Search ok
  1216. if($unseen_list[0]=='* SEARCH'){
  1217. //no results
  1218. $output=array();
  1219. }else{
  1220. $output=explode(" ",revright($unseen_list[0],9));
  1221. }
  1222. }else{
  1223. $output=false;
  1224. }
  1225. }
  1226. return $output;
  1227. }
  1228.  
  1229. /**
  1230. * Gets a list of messages from the server
  1231. *
  1232. * The structure of the returned array is:
  1233. *
  1234. * {{{
  1235. * array(
  1236. * (integer) {uid} => array(
  1237. * 'uid' => (integer) {a unique identifier for this message on this server},
  1238. * 'received' => (string) {date message was received},
  1239. * 'size' => (integer) {size of message in bytes},
  1240. * 'date' => (string) {date message was sent},
  1241. * 'from' => (string) {the from header value},
  1242. * 'subject' => (string) {the message subject},
  1243. * 'message_id' => (string) {optional - the message-id header value, should be globally unique},
  1244. * 'to' => (string) {optional - the to header value},
  1245. * 'in_reply_to' => (string) {optional - the in-reply-to header value}
  1246. * ), ...
  1247. * )
  1248. * }}}
  1249. *
  1250. * All values will have been decoded to UTF-8.
  1251. *
  1252. * @param integer $limit The number of messages to retrieve
  1253. * @param integer $page The page of messages to retrieve
  1254. * @return array A list of messages on the server - see method description for details
  1255. */
  1256.  
  1257.  
  1258. public function listMessages($limit=NULL, $page=NULL)
  1259. {
  1260. $this->connect();
  1261.  
  1262. if ($this->type == 'imap') {
  1263. if (!$limit) {
  1264. $start = 1;
  1265. $end = '*';
  1266. } else {
  1267. if (!$page) {
  1268. $page = 1;
  1269. }
  1270. $start = ($limit * ($page-1)) + 1;
  1271. $end = $start + $limit - 1;
  1272. }
  1273.  
  1274. $total_messages = 0;
  1275. $response = $this->write('STATUS "INBOX" (MESSAGES)');
  1276. foreach ($response as $line) {
  1277. if (preg_match('#^\s*\*\s+STATUS\s+"?INBOX"?\s+\((.*)\)$#', $line, $match)) {
  1278. $details = self::parseResponse($match[1], TRUE);
  1279. $total_messages = $details['messages'];
  1280. }
  1281. }
  1282.  
  1283. if ($start > $total_messages) {
  1284. return array();
  1285. }
  1286.  
  1287. if ($end > $total_messages) {
  1288. $end = $total_messages;
  1289. }
  1290.  
  1291. $output = array();
  1292. $response = $this->write('FETCH ' . $start . ':' . $end . ' (UID INTERNALDATE RFC822.SIZE ENVELOPE FLAGS)');
  1293. foreach ($response as $line) {
  1294. if (preg_match('#^\s*\*\s+(\d+)\s+FETCH\s+\((.*)\)$#', $line, $match)) {
  1295. $details = self::parseResponse($match[2], TRUE);
  1296. $info = array();
  1297.  
  1298. $info['uid'] = $details['uid'];
  1299. $info['received'] = self::cleanDate($details['internaldate']);
  1300. $info['size'] = $details['rfc822.size'];
  1301. $info[ 'flags'] = $details[ 'flags'];
  1302.  
  1303. $envelope = $details['envelope'];
  1304. $info['date'] = $envelope[0] != 'NIL' ? $envelope[0] : '';
  1305. $info['from'] = self::joinEmails($envelope[2]);
  1306. if (preg_match('#=\?[^\?]+\?[QB]\?[^\?]+\?=#', $envelope[1])) {
  1307. do {
  1308. $last_subject = $envelope[1];
  1309. $envelope[1] = preg_replace('#(=\?([^\?]+)\?[QB]\?[^\?]+\?=) (\s*=\?\2)#', '\1\3', $envelope[1]);
  1310. } while ($envelope[1] != $last_subject);
  1311. $info['subject'] = self::decodeHeader($envelope[1]);
  1312. } else {
  1313. $info['subject'] = $envelope[1] == 'NIL' ? '' : self::decodeHeader($envelope[1]);
  1314. }
  1315. if ($envelope[9] != 'NIL') {
  1316. $info['message_id'] = $envelope[9];
  1317. }
  1318. if ($envelope[5] != 'NIL') {
  1319. $info['to'] = self::joinEmails($envelope[5]);
  1320. }
  1321. if ($envelope[8] != 'NIL') {
  1322. $info['in_reply_to'] = $envelope[8];
  1323. }
  1324.  
  1325. $output[$info['uid']] = $info;
  1326. }
  1327. }
  1328.  
  1329. } elseif ($this->type == 'pop3') {
  1330. if (!$limit) {
  1331. $start = 1;
  1332. $end = NULL;
  1333. } else {
  1334. if (!$page) {
  1335. $page = 1;
  1336. }
  1337. $start = ($limit * ($page-1)) + 1;
  1338. $end = $start + $limit - 1;
  1339. }
  1340.  
  1341. $total_messages = 0;
  1342. $response = $this->write('STAT', 1);
  1343. preg_match('#^\+OK\s+(\d+)\s+#', $response[0], $match);
  1344. $total_messages = $match[1];
  1345.  
  1346. if ($start > $total_messages) {
  1347. return array();
  1348. }
  1349.  
  1350. if ($end === NULL || $end > $total_messages) {
  1351. $end = $total_messages;
  1352. }
  1353.  
  1354. $sizes = array();
  1355. $response = $this->write('LIST');
  1356. array_shift($response);
  1357. foreach ($response as $line) {
  1358. preg_match('#^(\d+)\s+(\d+)$#', $line, $match);
  1359. $sizes[$match[1]] = $match[2];
  1360. }
  1361.  
  1362. $output = array();
  1363. for ($i = $start; $i <= $end; $i++) {
  1364. $response = $this->write('TOP ' . $i . ' 1');
  1365. array_shift($response);
  1366. $value = array_pop($response);
  1367. // Some servers add an extra blank line after the 1 requested line
  1368. if (trim($value) == '') {
  1369. array_pop($response);
  1370. }
  1371.  
  1372. $response = trim(join("\r\n", $response));
  1373. $headers = self::parseHeaders($response);
  1374. $output[$i] = array(
  1375. 'uid' => $i,
  1376. 'received' => self::cleanDate(preg_replace('#^.*;\s*([^;]+)$#', '\1', $headers['received'][0])),
  1377. 'size' => $sizes[$i],
  1378. 'date' => $headers['date'],
  1379. 'from' => self::joinEmails(array($headers['from'])),
  1380. 'subject' => isset($headers['subject']) ? $headers['subject'] : ''
  1381. );
  1382. if (isset($headers['message-id'])) {
  1383. $output[$i]['message_id'] = $headers['message-id'];
  1384. }
  1385. if (isset($headers['to'])) {
  1386. $output[$i]['to'] = self::joinEmails($headers['to']);
  1387. }
  1388. if (isset($headers['in-reply-to'])) {
  1389. $output[$i]['in_reply_to'] = $headers['in-reply-to'];
  1390. }
  1391. }
  1392. }
  1393.  
  1394. return $output;
  1395. }
  1396.  
  1397. public function unboxMessage($message){
  1398. $savepath='upload/'.$message["tmpid"];
  1399. $boxedmessages=array();
  1400. $attachments=array();
  1401. if(isset($message['attachment'])){
  1402. $a=$message['attachment'];
  1403. $j=0;
  1404. $ark=array_keys($a);
  1405. while($j<count($a)){
  1406. $ac=$a[$ark[$j]];
  1407. switch($ac['mimetype']){
  1408. case 'message/rfc822':
  1409. $message2=self::parseMessage($ac['data']);
  1410. $message2["tmpid"]=$message["tmpid"];
  1411. $message2=self::unboxMessage($message2);
  1412. $boxedmessages[]=$message2;
  1413. break;
  1414. case 'message/delivery-status':
  1415. //those could be useful for further processing of mail delivery failures using the Final-Recipient: RFC822; <mail address> part of the attachment
  1416. break;
  1417. case 'message/disposition-notification':
  1418. // Return receipt
  1419. break;
  1420. default:
  1421. if($ac['filename']!='@'){
  1422. //mind the retarded MS Exchange Archiving that puts a fake "@" attachment on archived messages that had one - both in Outlook and this IMAP lib --
  1423. self::saveFileAttach($savepath,$ac);
  1424. $attachments[]=$ac['filename'];
  1425. }
  1426. break;
  1427. }
  1428. $j++;
  1429. }
  1430. unset($message['attachment']);
  1431. }
  1432. if(isset($message['related'])){
  1433. $a=$message['related'];
  1434. $j=0;
  1435. $ark=array_keys($a);
  1436. while($j<count($a)){
  1437. $ac=$a[$ark[$j]];
  1438. self::saveFileAttach($savepath,$ac);
  1439. $attachments[]=$ac['filename'];
  1440. $j++;
  1441. }
  1442. unset($message['related']);
  1443. }
  1444. $message['attachments']['messages']=$boxedmessages;
  1445. $message['attachments']['files']=$attachments;
  1446. return $message;
  1447. }
  1448.  
  1449. public function mails2Array($messages){
  1450. $processed=array();
  1451. $i=0;
  1452. while($i<count($messages)){
  1453. $uid=$messages[$i];
  1454. $message = self::fetchMessage($uid);
  1455. $message['from']=$message['headers']['from'];
  1456. $message['to']=$message['headers']['to'];
  1457. if(isset($message['headers']['cc'])){
  1458. $message['cc']=$message['headers']['cc'];
  1459. }
  1460. $message['subject']=$message['headers']['subject'];
  1461. $message["tmpid"]=makerealuid(rand());
  1462. $message = self::unboxMessage($message);
  1463. unset($message['headers']);
  1464. $processed[$i]=$message;
  1465. $i++;
  1466. }
  1467. return $processed;
  1468. }
  1469.  
  1470. private function saveFileAttach($path,$attachment){
  1471. if(!is_dir($path)){
  1472. mkdir($path);
  1473. }
  1474. $fp=fopen($path."/".$attachment['filename'],"w");
  1475. fputs($fp,$attachment['data']);
  1476. fclose($fp);
  1477. }
  1478.  
  1479. /**
  1480. * Reads responses from the server
  1481. *
  1482. * @param integer|string $expect The expected number of lines of response or a regex of the last line
  1483. * @return array The lines of response from the server
  1484. */
  1485. private function read($expect=NULL)
  1486. {
  1487. $read = array($this->connection);
  1488. $write = NULL;
  1489. $except = NULL;
  1490. $response = array();
  1491.  
  1492. // PHP 5.2.0 to 5.2.5 has a bug on amd64 linux where stream_select()
  1493. // fails, so we have to fake it - http://bugs.php.net/bug.php?id=42682
  1494. static $broken_select = NULL;
  1495. if ($broken_select === NULL) {
  1496. $broken_select = strpos(php_uname('m'), '64') !== FALSE && fCore::checkVersion('5.2.0') && !fCore::checkVersion('5.2.6');
  1497. }
  1498.  
  1499. // Fixes an issue with stream_select throwing a warning on PHP 5.3 on Windows
  1500. if (fCore::checkOS('windows') && fCore::checkVersion('5.3.0')) {
  1501. $select = @stream_select($read, $write, $except, $this->timeout);
  1502.  
  1503. } elseif ($broken_select) {
  1504. $broken_select_buffer = NULL;
  1505. $start_time = microtime(TRUE);
  1506. $i = 0;
  1507. do {
  1508. if ($i) {
  1509. usleep(50000);
  1510. }
  1511. $char = fgetc($this->connection);
  1512. if ($char != "\x00" && $char !== FALSE) {
  1513. $broken_select_buffer = $char;
  1514. }
  1515. $i++;
  1516. } while ($broken_select_buffer === NULL && microtime(TRUE) - $start_time < $this->timeout);
  1517. $select = $broken_select_buffer !== NULL;
  1518.  
  1519. } else {
  1520. $select = stream_select($read, $write, $except, $this->timeout);
  1521. }
  1522.  
  1523. if ($select) {
  1524. while (!feof($this->connection)) {
  1525. $line = fgets($this->connection);
  1526. if ($line === FALSE) {
  1527. break;
  1528. }
  1529. $line = substr($line, 0, -2);
  1530.  
  1531. // When we fake select, we have to handle what we've retrieved
  1532. if ($broken_select && $broken_select_buffer !== NULL) {
  1533. $line = $broken_select_buffer . $line;
  1534. $broken_select_buffer = NULL;
  1535. }
  1536.  
  1537. $response[] = $line;
  1538.  
  1539. // Automatically stop at the termination octet or a bad response
  1540. if ($this->type == 'pop3' && ($line == '.' || (count($response) == 1 && $response[0][0] == '-'))) {
  1541. break;
  1542. }
  1543.  
  1544. if ($expect !== NULL) {
  1545. $matched_number = is_int($expect) && sizeof($response) == $expect;
  1546. $matched_regex = is_string($expect) && preg_match($expect, $line);
  1547. if ($matched_number || $matched_regex) {
  1548. break;
  1549. }
  1550. }
  1551. }
  1552. }
  1553. if (fCore::getDebug($this->debug)) {
  1554. fCore::debug("Received:\n" . join("\r\n", $response), $this->debug);
  1555. }
  1556.  
  1557. if ($this->type == 'pop3') {
  1558. // Remove the termination octet
  1559. if ($response && $response[sizeof($response)-1] == '.') {
  1560. $response = array_slice($response, 0, -1);
  1561. }
  1562. // Remove byte-stuffing
  1563. $lines = count($response);
  1564. for ($i = 0; $i < $lines; $i++) {
  1565. if (strlen($response[$i]) && $response[$i][0] == '.') {
  1566. $response[$i] = substr($response[$i], 1);
  1567. }
  1568. }
  1569. }
  1570.  
  1571. return $response;
  1572. }
  1573.  
  1574.  
  1575. /**
  1576. * Sends commands to the IMAP or POP3 server
  1577. *
  1578. * @param string $command The command to send
  1579. * @param integer $expected The number of lines or regex expected for a POP3 command
  1580. * @return array The response from the server
  1581. */
  1582. private function write($command, $expected=NULL)
  1583. {
  1584. if (!$this->connection) {
  1585. throw new fProgrammerException('Unable to send data since the connection has already been closed');
  1586. }
  1587.  
  1588. if ($this->type == 'imap') {
  1589. $identifier = 'a' . str_pad($this->command_num++, 4, '0', STR_PAD_LEFT);
  1590. $command = $identifier . ' ' . $command;
  1591. }
  1592.  
  1593. if (substr($command, -2) != "\r\n") {
  1594. $command .= "\r\n";
  1595. }
  1596.  
  1597. if (fCore::getDebug($this->debug)) {
  1598. fCore::debug("Sending:\n" . trim($command), $this->debug);
  1599. }
  1600.  
  1601. $res = fwrite($this->connection, $command);
  1602.  
  1603. if ($res === FALSE || $res === 0) {
  1604. throw new fConnectivityException(
  1605. 'Unable to write data to %1$s server %2$s on port %3$s',
  1606. strtoupper($this->type),
  1607. $this->host,
  1608. $this->port
  1609. );
  1610. }
  1611.  
  1612. if ($this->type == 'imap') {
  1613. return $this->read('#^' . $identifier . '#');
  1614. } elseif ($this->type == 'pop3') {
  1615. return $this->read($expected);
  1616. }
  1617. }
  1618. }
  1619.  
  1620. /**
  1621. * Copyright (c) 2010-2011 Will Bond <will@flourishlib.com>
  1622. *
  1623. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1624. * of this software and associated documentation files (the "Software"), to deal
  1625. * in the Software without restriction, including without limitation the rights
  1626. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1627. * copies of the Software, and to permit persons to whom the Software is
  1628. * furnished to do so, subject to the following conditions:
  1629. *
  1630. * The above copyright notice and this permission notice shall be included in
  1631. * all copies or substantial portions of the Software.
  1632. *
  1633. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1634. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1635. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1636. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1637. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1638. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1639. * THE SOFTWARE.
  1640. */
  1641.  
  1642. <?php
  1643. function right($s,$c){
  1644. return mb_substr($s, mb_strlen($s)-$c,$c);
  1645. }
  1646.  
  1647. function revright($s,$c){
  1648. $s=right($s,mb_strlen($s)-$c);
  1649. return $s;
  1650. }
  1651.  
  1652. function makelocalprefix(){
  1653. $mid=15234567;
  1654. $pid=getmypid();
  1655. $tid=0001; //this is to be replaced by the "get thread id" function of your multithread framework thingy
  1656. $time=microtime();
  1657. $timeval=substr($time,11).substr($time,2,6);
  1658. return sprintf("%06s",base_convert($mid,10,16)) . "-" . sprintf("%05s",base_convert($pid.$tid,10,16)) ."-" . sprintf("%013s",base_convert($timeval,10,16)) . "-";
  1659. }
  1660.  
  1661. function makerealuid($oc){
  1662. // function overhead sucks .. wish there was an inline for php cuz it's like 1.5x time for no reason -another good reason to put it in a C lib
  1663. global $localprefix;
  1664. if(!isset($localprefix) || $localprefix==""){
  1665. $localprefix=makelocalprefix();
  1666. }
  1667. return $localprefix . sprintf("%08s",base_convert($oc,10,16));
  1668. }
  1669. ?>
  1670. /* this here is mine (c) Morg. or whatever. You can use it if you want to- it's better and faster than PHP's UUID so why not
Add Comment
Please, Sign In to add comment