PageRenderTime 52ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/base/lib/flourishlib/fMailbox.php

https://bitbucket.org/thanhtungnguyenphp/monitos
PHP | 1466 lines | 905 code | 195 blank | 366 comment | 265 complexity | 7fe717e6c5ed7b64d4f17ed56aa60d80 MD5 | raw file
  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.0b11
  16. * @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]
  17. * @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]
  18. * @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]
  19. * @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]
  20. * @changes 1.0.0b7 Fixed a typo in ::read() [wb, 2010-09-07]
  21. * @changes 1.0.0b6 Fixed a typo from 1.0.0b4 [wb, 2010-07-21]
  22. * @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]
  23. * @changes 1.0.0b4 Added code to handle emails without an explicit `Content-type` header [wb, 2010-06-04]
  24. * @changes 1.0.0b3 Added missing static method callback constants [wb, 2010-05-11]
  25. * @changes 1.0.0b2 Added the missing ::enableDebugging() [wb, 2010-05-05]
  26. * @changes 1.0.0b The initial implementation [wb, 2010-05-05]
  27. */
  28. class fMailbox
  29. {
  30. const addSMIMEPair = 'fMailbox::addSMIMEPair';
  31. const parseMessage = 'fMailbox::parseMessage';
  32. const reset = 'fMailbox::reset';
  33. /**
  34. * S/MIME certificates and private keys for verification and decryption
  35. *
  36. * @var array
  37. */
  38. static private $smime_pairs = array();
  39. /**
  40. * Adds an S/MIME certificate, or certificate + private key pair for verification and decryption of S/MIME messages
  41. *
  42. * @param string $email_address The email address the certificate or private key is for
  43. * @param fFile|string $certificate_file The file the S/MIME certificate is stored in - required for verification and decryption
  44. * @param fFile $private_key_file The file the S/MIME private key is stored in - required for decryption only
  45. * @param string $private_key_password The password for the private key
  46. * @return void
  47. */
  48. static public function addSMIMEPair($email_address, $certificate_file, $private_key_file=NULL, $private_key_password=NULL)
  49. {
  50. if ($private_key_file !== NULL && !$private_key_file instanceof fFile) {
  51. $private_key_file = new fFile($private_key_file);
  52. }
  53. if (!$certificate_file instanceof fFile) {
  54. $certificate_file = new fFile($certificate_file);
  55. }
  56. self::$smime_pairs[strtolower($email_address)] = array(
  57. 'certificate' => $certificate_file,
  58. 'private_key' => $private_key_file,
  59. 'password' => $private_key_password
  60. );
  61. }
  62. /**
  63. * Takes a date, removes comments and cleans up some common formatting inconsistencies
  64. *
  65. * @param string $date The date to clean
  66. * @return string The cleaned date
  67. */
  68. static private function cleanDate($date)
  69. {
  70. $date = preg_replace('#\([^)]+\)#', ' ', trim($date));
  71. $date = preg_replace('#\s+#', ' ', $date);
  72. $date = preg_replace('#(\d+)-([a-z]+)-(\d{4})#i', '\1 \2 \3', $date);
  73. $date = preg_replace('#^[a-z]+\s*,\s*#i', '', trim($date));
  74. return trim($date);
  75. }
  76. /**
  77. * Decodes encoded-word headers of any encoding into raw UTF-8
  78. *
  79. * @param string $text The header value to decode
  80. * @return string The decoded UTF-8
  81. */
  82. static private function decodeHeader($text)
  83. {
  84. $parts = preg_split('#("[^"]+"|=\?[^\?]+\?[QB]\?[^\?]+\?=)#i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
  85. $part_with_encoding = array();
  86. $output = '';
  87. foreach ($parts as $part) {
  88. if ($part === '') {
  89. continue;
  90. }
  91. if (preg_match_all('#=\?([^\?]+)\?([QB])\?([^\?]+)\?=#i', $part, $matches, PREG_SET_ORDER)) {
  92. foreach ($matches as $match) {
  93. if (strtoupper($match[2]) == 'Q') {
  94. $part_string = rawurldecode(strtr(
  95. $match[3],
  96. array(
  97. '=' => '%',
  98. '_' => ' '
  99. )
  100. ));
  101. } else {
  102. $part_string = base64_decode($match[3]);
  103. }
  104. $lower_encoding = strtolower($match[1]);
  105. $last_key = count($part_with_encoding) - 1;
  106. if (isset($part_with_encoding[$last_key]) && $part_with_encoding[$last_key]['encoding'] == $lower_encoding) {
  107. $part_with_encoding[$last_key]['string'] .= $part_string;
  108. } else {
  109. $part_with_encoding[] = array('encoding' => $lower_encoding, 'string' => $part_string);
  110. }
  111. }
  112. } else {
  113. $last_key = count($part_with_encoding) - 1;
  114. if (isset($part_with_encoding[$last_key]) && $part_with_encoding[$last_key]['encoding'] == 'iso-8859-1') {
  115. $part_with_encoding[$last_key]['string'] .= $part;
  116. } else {
  117. $part_with_encoding[] = array('encoding' => 'iso-8859-1', 'string' => $part);
  118. }
  119. }
  120. }
  121. foreach ($part_with_encoding as $part) {
  122. $output .= iconv($part['encoding'], 'UTF-8', $part['string']);
  123. }
  124. return $output;
  125. }
  126. /**
  127. * Handles an individual part of a multipart message
  128. *
  129. * @param array $info An array of information about the message
  130. * @param array $structure An array describing the structure of the message
  131. * @return array The modified $info array
  132. */
  133. static private function handlePart($info, $structure)
  134. {
  135. if ($structure['type'] == 'multipart') {
  136. foreach ($structure['parts'] as $part) {
  137. $info = self::handlePart($info, $part);
  138. }
  139. return $info;
  140. }
  141. if ($structure['type'] == 'application' && in_array($structure['subtype'], array('pkcs7-mime', 'x-pkcs7-mime'))) {
  142. $to = NULL;
  143. if (isset($info['headers']['to'][0])) {
  144. $to = $info['headers']['to'][0]['mailbox'];
  145. if (!empty($info['headers']['to'][0]['host'])) {
  146. $to .= '@' . $info['headers']['to'][0]['host'];
  147. }
  148. }
  149. if ($to && !empty(self::$smime_pairs[$to]['private_key'])) {
  150. if (self::handleSMIMEDecryption($info, $structure, self::$smime_pairs[$to])) {
  151. return $info;
  152. }
  153. }
  154. }
  155. if ($structure['type'] == 'application' && in_array($structure['subtype'], array('pkcs7-signature', 'x-pkcs7-signature'))) {
  156. $from = NULL;
  157. if (isset($info['headers']['from'])) {
  158. $from = $info['headers']['from']['mailbox'];
  159. if (!empty($info['headers']['from']['host'])) {
  160. $from .= '@' . $info['headers']['from']['host'];
  161. }
  162. }
  163. if ($from && !empty(self::$smime_pairs[$from]['certificate'])) {
  164. if (self::handleSMIMEVerification($info, $structure, self::$smime_pairs[$from])) {
  165. return $info;
  166. }
  167. }
  168. }
  169. $data = $structure['data'];
  170. if ($structure['encoding'] == 'base64') {
  171. $content = '';
  172. foreach (explode("\r\n", $data) as $line) {
  173. $content .= base64_decode($line);
  174. }
  175. } elseif ($structure['encoding'] == 'quoted-printable') {
  176. $content = quoted_printable_decode($data);
  177. } else {
  178. $content = $data;
  179. }
  180. if ($structure['type'] == 'text') {
  181. $charset = 'iso-8859-1';
  182. foreach ($structure['type_fields'] as $field => $value) {
  183. if (strtolower($field) == 'charset') {
  184. $charset = $value;
  185. break;
  186. }
  187. }
  188. $content = iconv($charset, 'UTF-8', $content);
  189. if ($structure['subtype'] == 'html') {
  190. $content = preg_replace('#(content=(["\'])text/html\s*;\s*charset=(["\']?))' . preg_quote($charset, '#') . '(\3\2)#i', '\1utf-8\4', $content);
  191. }
  192. }
  193. // This indicates a content-id which is used for multipart/related
  194. if ($structure['content_id']) {
  195. if (!isset($info['related'])) {
  196. $info['related'] = array();
  197. }
  198. $cid = $structure['content_id'][0] == '<' ? substr($structure['content_id'], 1, -1) : $structure['content_id'];
  199. $info['related']['cid:' . $cid] = array(
  200. 'mimetype' => $structure['type'] . '/' . $structure['subtype'],
  201. 'data' => $content
  202. );
  203. return $info;
  204. }
  205. $has_disposition = !empty($structure['disposition']);
  206. $is_text = $structure['type'] == 'text' && $structure['subtype'] == 'plain';
  207. $is_html = $structure['type'] == 'text' && $structure['subtype'] == 'html';
  208. // If the part doesn't have a disposition and is not the default text or html, set the disposition to inline
  209. if (!$has_disposition && ((!$is_text || !empty($info['text'])) && (!$is_html || !empty($info['html'])))) {
  210. $is_web_image = $structure['type'] == 'image' && in_array($structure['subtype'], array('gif', 'png', 'jpeg', 'pjpeg'));
  211. $structure['disposition'] = $is_text || $is_html || $is_web_image ? 'inline' : 'attachment';
  212. $structure['disposition_fields'] = array();
  213. $has_disposition = TRUE;
  214. }
  215. // Attachments or inline content
  216. if ($has_disposition) {
  217. $filename = '';
  218. foreach ($structure['disposition_fields'] as $field => $value) {
  219. if (strtolower($field) == 'filename') {
  220. $filename = $value;
  221. break;
  222. }
  223. }
  224. foreach ($structure['type_fields'] as $field => $value) {
  225. if (strtolower($field) == 'name') {
  226. $filename = $value;
  227. break;
  228. }
  229. }
  230. // This automatically handles primary content that has a content-disposition header on it
  231. if ($structure['disposition'] == 'inline' && $filename === '') {
  232. if ($is_text && !isset($info['text'])) {
  233. $info['text'] = $content;
  234. return $info;
  235. }
  236. if ($is_html && !isset($info['html'])) {
  237. $info['html'] = $content;
  238. return $info;
  239. }
  240. }
  241. if (!isset($info[$structure['disposition']])) {
  242. $info[$structure['disposition']] = array();
  243. }
  244. $info[$structure['disposition']][] = array(
  245. 'filename' => $filename,
  246. 'mimetype' => $structure['type'] . '/' . $structure['subtype'],
  247. 'data' => $content
  248. );
  249. return $info;
  250. }
  251. if ($is_text) {
  252. $info['text'] = $content;
  253. return $info;
  254. }
  255. if ($is_html) {
  256. $info['html'] = $content;
  257. return $info;
  258. }
  259. }
  260. /**
  261. * Tries to decrypt an S/MIME message using a private key
  262. *
  263. * @param array &$info The array of information about a message
  264. * @param array $structure The structure of this part
  265. * @param array $smime_pair An associative array containing an S/MIME certificate, private key and password
  266. * @return boolean If the message was decrypted
  267. */
  268. static private function handleSMIMEDecryption(&$info, $structure, $smime_pair)
  269. {
  270. $plaintext_file = tempnam('', '__fMailbox_');
  271. $ciphertext_file = tempnam('', '__fMailbox_');
  272. $headers = array();
  273. $headers[] = "Content-Type: " . $structure['type'] . '/' . $structure['subtype'];
  274. $headers[] = "Content-Transfer-Encoding: " . $structure['encoding'];
  275. $header = "Content-Disposition: " . $structure['disposition'];
  276. foreach ($structure['disposition_fields'] as $field => $value) {
  277. $header .= '; ' . $field . '="' . $value . '"';
  278. }
  279. $headers[] = $header;
  280. file_put_contents($ciphertext_file, join("\r\n", $headers) . "\r\n\r\n" . $structure['data']);
  281. $private_key = openssl_pkey_get_private(
  282. $smime_pair['private_key']->read(),
  283. $smime_pair['password']
  284. );
  285. $certificate = $smime_pair['certificate']->read();
  286. $result = openssl_pkcs7_decrypt($ciphertext_file, $plaintext_file, $certificate, $private_key);
  287. unlink($ciphertext_file);
  288. if (!$result) {
  289. unlink($plaintext_file);
  290. return FALSE;
  291. }
  292. $contents = file_get_contents($plaintext_file);
  293. $info['raw_message'] = $contents;
  294. $info = self::handlePart($info, self::parseStructure($contents));
  295. $info['decrypted'] = TRUE;
  296. unlink($plaintext_file);
  297. return TRUE;
  298. }
  299. /**
  300. * Takes a message with an S/MIME signature and verifies it if possible
  301. *
  302. * @param array &$info The array of information about a message
  303. * @param array $structure
  304. * @param array $smime_pair An associative array containing an S/MIME certificate file
  305. * @return boolean If the message was verified
  306. */
  307. static private function handleSMIMEVerification(&$info, $structure, $smime_pair)
  308. {
  309. $certificates_file = tempnam('', '__fMailbox_');
  310. $ciphertext_file = tempnam('', '__fMailbox_');
  311. file_put_contents($ciphertext_file, $info['raw_message']);
  312. $result = openssl_pkcs7_verify(
  313. $ciphertext_file,
  314. PKCS7_NOINTERN | PKCS7_NOVERIFY,
  315. $certificates_file,
  316. array(),
  317. $smime_pair['certificate']->getPath()
  318. );
  319. unlink($ciphertext_file);
  320. unlink($certificates_file);
  321. if (!$result || $result === -1) {
  322. return FALSE;
  323. }
  324. $info['verified'] = TRUE;
  325. return TRUE;
  326. }
  327. /**
  328. * Joins parsed emails into a comma-delimited string
  329. *
  330. * @param array $emails An array of emails split into personal, mailbox and host parts
  331. * @return string An comma-delimited list of emails
  332. */
  333. static private function joinEmails($emails)
  334. {
  335. $output = '';
  336. foreach ($emails as $email) {
  337. if ($output) { $output .= ', '; }
  338. if (!isset($email[0])) {
  339. $email[0] = !empty($email['personal']) ? $email['personal'] : 'NIL';
  340. $email[2] = $email['mailbox'];
  341. $email[3] = !empty($email['host']) ? $email['host'] : 'NIL';
  342. }
  343. if ($email[0] != 'NIL') {
  344. $output .= '"' . self::decodeHeader($email[0]) . '" <';
  345. }
  346. $output .= $email[2];
  347. if ($email[3] != 'NIL') {
  348. $output .= '@' . $email[3];
  349. }
  350. if ($email[0] != 'NIL') {
  351. $output .= '>';
  352. }
  353. }
  354. return $output;
  355. }
  356. /**
  357. * Parses a string representation of an email into the persona, mailbox and host parts
  358. *
  359. * @param string $string The email string to parse
  360. * @return array An associative array with the key `mailbox`, and possibly `host` and `personal`
  361. */
  362. static private function parseEmail($string)
  363. {
  364. $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])\])';
  365. $name_regex = '((?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*)(?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*)';
  366. if (preg_match('~^[ \t]*' . $name_regex . '[ \t]*<[ \t]*' . $email_regex . '[ \t]*>[ \t]*$~ixD', $string, $match)) {
  367. $match[1] = trim($match[1]);
  368. if ($match[1][0] == '"' && substr($match[1], -1) == '"') {
  369. $match[1] = substr($match[1], 1, -1);
  370. }
  371. return array('personal' => $match[1], 'mailbox' => $match[2], 'host' => $match[3]);
  372. } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . '(?:[ \t]*>)?[ \t]*$~ixD', $string, $match)) {
  373. return array('mailbox' => $match[1], 'host' => $match[2]);
  374. // This handles the outdated practice of including the personal
  375. // part of the email in a comment after the email address
  376. } elseif (preg_match('~^[ \t]*(?:<[ \t]*)?' . $email_regex . '(?:[ \t]*>)?[ \t]*\(([^)]+)\)[ \t]*$~ixD', $string, $match)) {
  377. $match[3] = trim($match[1]);
  378. if ($match[3][0] == '"' && substr($match[3], -1) == '"') {
  379. $match[3] = substr($match[3], 1, -1);
  380. }
  381. return array('personal' => $match[3], 'mailbox' => $match[1], 'host' => $match[2]);
  382. }
  383. if (strpos($string, '@') !== FALSE) {
  384. list ($mailbox, $host) = explode('@', $string, 2);
  385. return array('mailbox' => $mailbox, 'host' => $host);
  386. }
  387. return array('mailbox' => $string, 'host' => '');
  388. }
  389. /**
  390. * Parses full email headers into an associative array
  391. *
  392. * @param string $headers The header to parse
  393. * @param string $filter Remove any headers that match this
  394. * @return array The parsed headers
  395. */
  396. static private function parseHeaders($headers, $filter=NULL)
  397. {
  398. $header_lines = preg_split("#\r\n(?!\s)#", trim($headers));
  399. $single_email_fields = array('from', 'sender', 'reply-to');
  400. $multi_email_fields = array('to', 'cc');
  401. $additional_info_fields = array('content-type', 'content-disposition');
  402. $headers = array();
  403. foreach ($header_lines as $header_line) {
  404. $header_line = preg_replace("#\r\n\s+#", '', $header_line);
  405. $header_line = self::decodeHeader($header_line);
  406. list ($header, $value) = preg_split('#:\s*#', $header_line, 2);
  407. $header = strtolower($header);
  408. if (strpos($header, $filter) !== FALSE) {
  409. continue;
  410. }
  411. $is_single_email = in_array($header, $single_email_fields);
  412. $is_multi_email = in_array($header, $multi_email_fields);
  413. $is_additional_info_field = in_array($header, $additional_info_fields);
  414. if ($is_additional_info_field) {
  415. $pieces = preg_split('#;\s*#', $value, 2);
  416. $value = $pieces[0];
  417. $headers[$header] = array('value' => $value);
  418. $fields = array();
  419. if (!empty($pieces[1])) {
  420. preg_match_all('#(\w+)=("([^"]+)"|([^\s;]+))(?=;|$)#', $pieces[1], $matches, PREG_SET_ORDER);
  421. foreach ($matches as $match) {
  422. $fields[$match[1]] = !empty($match[4]) ? $match[4] : $match[3];
  423. }
  424. }
  425. $headers[$header]['fields'] = $fields;
  426. } elseif ($is_single_email) {
  427. $headers[$header] = self::parseEmail($value);
  428. } elseif ($is_multi_email) {
  429. $strings = array();
  430. preg_match_all('#"[^"]+?"#', $value, $matches, PREG_SET_ORDER);
  431. foreach ($matches as $i => $match) {
  432. $strings[] = $match[0];
  433. $value = preg_replace('#' . preg_quote($match[0], '#') . '#', ':string' . sizeof($strings), $value, 1);
  434. }
  435. preg_match_all('#\([^)]+?\)#', $value, $matches, PREG_SET_ORDER);
  436. foreach ($matches as $i => $match) {
  437. $strings[] = $match[0];
  438. $value = preg_replace('#' . preg_quote($match[0], '#') . '#', ':string' . sizeof($strings), $value, 1);
  439. }
  440. $emails = explode(',', $value);
  441. array_map('trim', $emails);
  442. foreach ($strings as $i => $string) {
  443. $emails = preg_replace(
  444. '#:string' . ($i+1) . '\b#',
  445. strtr($string, array('\\' => '\\\\', '$' => '\\$')),
  446. $emails,
  447. 1
  448. );
  449. }
  450. $headers[$header] = array();
  451. foreach ($emails as $email) {
  452. $headers[$header][] = self::parseEmail($email);
  453. }
  454. } elseif ($header == 'references') {
  455. $headers[$header] = preg_split('#(?<=>)\s+(?=<)#', $value);
  456. } elseif ($header == 'received') {
  457. if (!isset($headers[$header])) {
  458. $headers[$header] = array();
  459. }
  460. $headers[$header][] = preg_replace('#\s+#', ' ', $value);
  461. } else {
  462. $headers[$header] = $value;
  463. }
  464. }
  465. return $headers;
  466. }
  467. /**
  468. * Parses a MIME message into an associative array of information
  469. *
  470. * The output includes the following keys:
  471. *
  472. * - `'received'`: The date the message was received by the server
  473. * - `'headers'`: An associative array of mail headers, the keys are the header names, in lowercase
  474. *
  475. * And one or more of the following:
  476. *
  477. * - `'text'`: The plaintext body
  478. * - `'html'`: The HTML body
  479. * - `'attachment'`: An array of attachments, each containing:
  480. * - `'filename'`: The name of the file
  481. * - `'mimetype'`: The mimetype of the file
  482. * - `'data'`: The raw contents of the file
  483. * - `'inline'`: An array of inline files, each containing:
  484. * - `'filename'`: The name of the file
  485. * - `'mimetype'`: The mimetype of the file
  486. * - `'data'`: The raw contents of the file
  487. * - `'related'`: An associative array of related files, such as embedded images, with the key `'cid:{content-id}'` and an array value containing:
  488. * - `'mimetype'`: The mimetype of the file
  489. * - `'data'`: The raw contents of the file
  490. * - `'verified'`: If the message contents were verified via an S/MIME certificate - if not verified the smime.p7s will be listed as an attachment
  491. * - `'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
  492. *
  493. * All values in `headers`, `text` and `body` will have been decoded to
  494. * UTF-8. Files in the `attachment`, `inline` and `related` array will all
  495. * retain their original encodings.
  496. *
  497. * @param string $message The full source of the email message
  498. * @param boolean $convert_newlines If `\r\n` should be converted to `\n` in the `text` and `html` parts the message
  499. * @return array The parsed email message - see method description for details
  500. */
  501. static public function parseMessage($message, $convert_newlines=FALSE)
  502. {
  503. $info = array();
  504. list ($headers, $body) = explode("\r\n\r\n", $message, 2);
  505. $parsed_headers = self::parseHeaders($headers);
  506. $info['received'] = self::cleanDate(preg_replace('#^.*;\s*([^;]+)$#', '\1', $parsed_headers['received'][0]));
  507. $info['headers'] = array();
  508. foreach ($parsed_headers as $header => $value) {
  509. if (substr($header, 0, 8) == 'content-') {
  510. continue;
  511. }
  512. $info['headers'][$header] = $value;
  513. }
  514. $info['raw_headers'] = $headers;
  515. $info['raw_message'] = $message;
  516. $info = self::handlePart($info, self::parseStructure($body, $parsed_headers));
  517. unset($info['raw_message']);
  518. unset($info['raw_headers']);
  519. if ($convert_newlines) {
  520. if (isset($info['text'])) {
  521. $info['text'] = str_replace("\r\n", "\n", $info['text']);
  522. }
  523. if (isset($info['html'])) {
  524. $info['html'] = str_replace("\r\n", "\n", $info['html']);
  525. }
  526. }
  527. if (isset($info['text'])) {
  528. $info['text'] = preg_replace('#\r?\n$#D', '', $info['text']);
  529. }
  530. if (isset($info['html'])) {
  531. $info['html'] = preg_replace('#\r?\n$#D', '', $info['html']);
  532. }
  533. return $info;
  534. }
  535. /**
  536. * Takes a response from an IMAP command and parses it into a
  537. * multi-dimensional array
  538. *
  539. * @param string $text The IMAP command response
  540. * @param boolean $top_level If we are parsing the top level
  541. * @return array The parsed representation of the response text
  542. */
  543. static private function parseResponse($text, $top_level=FALSE)
  544. {
  545. $regex = '[\\\\\w.\[\]]+|"([^"\\\\]+|\\\\"|\\\\\\\\)*"|\((?:(?1)[ \t]*)*\)';
  546. if (preg_match('#\{(\d+)\}#', $text, $match)) {
  547. $regex = '\{' . $match[1] . '\}\r\n.{' . ($match[1]) . '}|' . $regex;
  548. }
  549. preg_match_all('#(' . $regex . ')#s', $text, $matches, PREG_SET_ORDER);
  550. $output = array();
  551. foreach ($matches as $match) {
  552. if (substr($match[0], 0, 1) == '"') {
  553. $output[] = str_replace('\\"', '"', substr($match[0], 1, -1));
  554. } elseif (substr($match[0], 0, 1) == '(') {
  555. $output[] = self::parseResponse(substr($match[0], 1, -1));
  556. } elseif (substr($match[0], 0, 1) == '{') {
  557. $output[] = preg_replace('#^[^\r]+\r\n#', '', $match[0]);
  558. } else {
  559. $output[] = $match[0];
  560. }
  561. }
  562. if ($top_level) {
  563. $new_output = array();
  564. $total_size = count($output);
  565. for ($i = 0; $i < $total_size; $i = $i + 2) {
  566. $new_output[strtolower($output[$i])] = $output[$i+1];
  567. }
  568. $output = $new_output;
  569. }
  570. return $output;
  571. }
  572. /**
  573. * Takes the raw contents of a MIME message and creates an array that
  574. * describes the structure of the message
  575. *
  576. * @param string $data The contents to get the structure of
  577. * @param string $headers The parsed headers for the message - if not present they will be extracted from the `$data`
  578. * @return array The multi-dimensional, associative array containing the message structure
  579. */
  580. static private function parseStructure($data, $headers=NULL)
  581. {
  582. if (!$headers) {
  583. list ($headers, $data) = explode("\r\n\r\n", $data, 2);
  584. $headers = self::parseHeaders($headers);
  585. }
  586. if (!isset($headers['content-type'])) {
  587. $headers['content-type'] = array(
  588. 'value' => 'text/plain',
  589. 'fields' => array()
  590. );
  591. }
  592. list ($type, $subtype) = explode('/', strtolower($headers['content-type']['value']), 2);
  593. if ($type == 'multipart') {
  594. $structure = array(
  595. 'type' => $type,
  596. 'subtype' => $subtype,
  597. 'parts' => array()
  598. );
  599. $boundary = $headers['content-type']['fields']['boundary'];
  600. $start_pos = strpos($data, '--' . $boundary) + strlen($boundary) + 4;
  601. $end_pos = strrpos($data, '--' . $boundary . '--') - 2;
  602. $sub_contents = explode("\r\n--" . $boundary . "\r\n", substr(
  603. $data,
  604. $start_pos,
  605. $end_pos - $start_pos
  606. ));
  607. foreach ($sub_contents as $sub_content) {
  608. $structure['parts'][] = self::parseStructure($sub_content);
  609. }
  610. } else {
  611. $structure = array(
  612. 'type' => $type,
  613. 'type_fields' => !empty($headers['content-type']['fields']) ? $headers['content-type']['fields'] : array(),
  614. 'subtype' => $subtype,
  615. 'content_id' => isset($headers['content-id']) ? $headers['content-id'] : NULL,
  616. 'encoding' => isset($headers['content-transfer-encoding']) ? strtolower($headers['content-transfer-encoding']) : '8bit',
  617. 'disposition' => isset($headers['content-disposition']) ? strtolower($headers['content-disposition']['value']) : NULL,
  618. 'disposition_fields' => isset($headers['content-disposition']) ? $headers['content-disposition']['fields'] : array(),
  619. 'data' => $data
  620. );
  621. }
  622. return $structure;
  623. }
  624. /**
  625. * Resets the configuration of the class
  626. *
  627. * @internal
  628. *
  629. * @return void
  630. */
  631. static public function reset()
  632. {
  633. self::$smime_pairs = array();
  634. }
  635. /**
  636. * Takes an associative array and unfolds the keys and values so that the
  637. * result in an integer-indexed array of `0 => key1, 1 => value1, 2 => key2,
  638. * 3 => value2, ...`.
  639. *
  640. * @param array $array The array to unfold
  641. * @return array The unfolded array
  642. */
  643. static private function unfoldAssociativeArray($array)
  644. {
  645. $new_array = array();
  646. foreach ($array as $key => $value) {
  647. $new_array[] = $key;
  648. $new_array[] = $value;
  649. }
  650. return $new_array;
  651. }
  652. /**
  653. * A counter to use for generating command keys
  654. *
  655. * @var integer
  656. */
  657. private $command_num = 1;
  658. /**
  659. * The connection resource
  660. *
  661. * @var resource
  662. */
  663. private $connection;
  664. /**
  665. * If debugging has been enabled
  666. *
  667. * @var boolean
  668. */
  669. private $debug;
  670. /**
  671. * The server hostname or IP address
  672. *
  673. * @var string
  674. */
  675. private $host;
  676. /**
  677. * The password for the account
  678. *
  679. * @var string
  680. */
  681. private $password;
  682. /**
  683. * The port for the server
  684. *
  685. * @var integer
  686. */
  687. private $port;
  688. /**
  689. * If the connection to the server should be secure
  690. *
  691. * @var boolean
  692. */
  693. private $secure;
  694. /**
  695. * The timeout for the connection
  696. *
  697. * @var integer
  698. */
  699. private $timeout = 5;
  700. /**
  701. * The type of mailbox, `'imap'` or `'pop3'`
  702. *
  703. * @var string
  704. */
  705. private $type;
  706. /**
  707. * The username for the account
  708. *
  709. * @var string
  710. */
  711. private $username;
  712. /**
  713. * Configures the connection to the server
  714. *
  715. * Please note that the GMail POP3 server does not act like other POP3
  716. * servers and the GMail IMAP server should be used instead. GMail POP3 only
  717. * allows retrieving a message once - during future connections the email
  718. * in question will no longer be available.
  719. *
  720. * @param string $type The type of mailbox, `'pop3'` or `'imap'`
  721. * @param string $host The server hostname or IP address
  722. * @param string $username The user to log in as
  723. * @param string $password The user's password
  724. * @param integer $port The port to connect via - only required if non-standard
  725. * @param boolean $secure If SSL should be used for the connection - this requires the `openssl` extension
  726. * @param integer $timeout The timeout to use when connecting
  727. * @return fMailbox
  728. */
  729. public function __construct($type, $host, $username, $password, $port=NULL, $secure=FALSE, $timeout=NULL)
  730. {
  731. if ($timeout === NULL) {
  732. $timeout = ini_get('default_socket_timeout');
  733. }
  734. $valid_types = array('imap', 'pop3');
  735. if (!in_array($type, $valid_types)) {
  736. throw new fProgrammerException(
  737. 'The mailbox type specified, %1$s, in invalid. Must be one of: %2$s.',
  738. $type,
  739. join(', ', $valid_types)
  740. );
  741. }
  742. if ($port === NULL) {
  743. if ($type == 'imap') {
  744. $port = !$secure ? 143 : 993;
  745. } else {
  746. $port = !$secure ? 110 : 995;
  747. }
  748. }
  749. if ($secure && !extension_loaded('openssl')) {
  750. throw new fEnvironmentException(
  751. 'A secure connection was requested, but the %s extension is not installed',
  752. 'openssl'
  753. );
  754. }
  755. $this->type = $type;
  756. $this->host = $host;
  757. $this->username = $username;
  758. $this->password = $password;
  759. $this->port = $port;
  760. $this->secure = $secure;
  761. $this->timeout = $timeout;
  762. }
  763. /**
  764. * Disconnects from the server
  765. *
  766. * @return void
  767. */
  768. public function __destruct()
  769. {
  770. $this->close();
  771. }
  772. /**
  773. * Closes the connection to the server
  774. *
  775. * @return void
  776. */
  777. public function close()
  778. {
  779. if (!$this->connection) {
  780. return;
  781. }
  782. if ($this->type == 'imap') {
  783. $this->write('LOGOUT');
  784. } else {
  785. $this->write('QUIT', 1);
  786. }
  787. $this->connection = NULL;
  788. }
  789. /**
  790. * Connects to the server
  791. *
  792. * @return void
  793. */
  794. private function connect()
  795. {
  796. if ($this->connection) {
  797. return;
  798. }
  799. fCore::startErrorCapture(E_WARNING);
  800. $this->connection = fsockopen(
  801. $this->secure ? 'tls://' . $this->host : $this->host,
  802. $this->port,
  803. $error_number,
  804. $error_string,
  805. $this->timeout
  806. );
  807. foreach (fCore::stopErrorCapture('#ssl#i') as $error) {
  808. 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.');
  809. }
  810. if (!$this->connection) {
  811. throw new fConnectivityException('There was an error connecting to the server');
  812. }
  813. stream_set_timeout($this->connection, $this->timeout);
  814. if ($this->type == 'imap') {
  815. if (!$this->secure && extension_loaded('openssl')) {
  816. $response = $this->write('CAPABILITY');
  817. if (preg_match('#\bstarttls\b#i', $response[0])) {
  818. $this->write('STARTTLS');
  819. do {
  820. if (isset($res)) {
  821. sleep(0.1);
  822. }
  823. $res = stream_socket_enable_crypto($this->connection, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  824. } while ($res === 0);
  825. }
  826. }
  827. $response = $this->write('LOGIN ' . $this->username . ' ' . $this->password);
  828. if (!$response || !preg_match('#^[^ ]+\s+OK#', $response[count($response)-1])) {
  829. throw new fValidationException(
  830. 'The username and password provided were not accepted for the %1$s server %2$s on port %3$s',
  831. strtoupper($this->type),
  832. $this->host,
  833. $this->port
  834. );
  835. }
  836. $this->write('SELECT "INBOX"');
  837. } elseif ($this->type == 'pop3') {
  838. $response = $this->read(1);
  839. if (isset($response[0])) {
  840. if ($response[0][0] == '-') {
  841. throw new fConnectivityException(
  842. 'There was an error connecting to the POP3 server %1$s on port %2$s',
  843. $this->host,
  844. $this->port
  845. );
  846. }
  847. preg_match('#<[^@]+@[^>]+>#', $response[0], $match);
  848. }
  849. if (!$this->secure && extension_loaded('openssl')) {
  850. $response = $this->write('STLS', 1);
  851. if ($response[0][0] == '+') {
  852. do {
  853. if (isset($res)) {
  854. sleep(0.1);
  855. }
  856. $res = stream_socket_enable_crypto($this->connection, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  857. } while ($res === 0);
  858. if ($res === FALSE) {
  859. throw new fConnectivityException('Error establishing secure connection');
  860. }
  861. }
  862. }
  863. $authenticated = FALSE;
  864. if (isset($match[0])) {
  865. $response = $this->write('APOP ' . $this->username . ' ' . md5($match[0] . $this->password), 1);
  866. if (isset($response[0]) && $response[0][0] == '+') {
  867. $authenticated = TRUE;
  868. }
  869. }
  870. if (!$authenticated) {
  871. $response = $this->write('USER ' . $this->username, 1);
  872. if ($response[0][0] == '+') {
  873. $response = $this->write('PASS ' . $this->password, 1);
  874. if (isset($response[0][0]) && $response[0][0] == '+') {
  875. $authenticated = TRUE;
  876. }
  877. }
  878. }
  879. if (!$authenticated) {
  880. throw new fValidationException(
  881. 'The username and password provided were not accepted for the %1$s server %2$s on port %3$s',
  882. strtoupper($this->type),
  883. $this->host,
  884. $this->port
  885. );
  886. }
  887. }
  888. }
  889. /**
  890. * Deletes one or more messages from the server
  891. *
  892. * Passing more than one UID at a time is more efficient for IMAP mailboxes,
  893. * whereas POP3 mailboxes will see no difference in performance.
  894. *
  895. * @param integer|array $uid The UID(s) of the message(s) to delete
  896. * @return void
  897. */
  898. public function deleteMessages($uid)
  899. {
  900. $this->connect();
  901. settype($uid, 'array');
  902. if ($this->type == 'imap') {
  903. $this->write('UID STORE ' . join(',', $uid) . ' +FLAGS (\\Deleted)');
  904. $this->write('EXPUNGE');
  905. } elseif ($this->type == 'pop3') {
  906. foreach ($uid as $id) {
  907. $this->write('DELE ' . $id, 1);
  908. }
  909. }
  910. }
  911. /**
  912. * Sets if debug messages should be shown
  913. *
  914. * @param boolean $flag If debugging messages should be shown
  915. * @return void
  916. */
  917. public function enableDebugging($flag)
  918. {
  919. $this->debug = (boolean) $flag;
  920. }
  921. /**
  922. * Retrieves a single message from the server
  923. *
  924. * The output includes the following keys:
  925. *
  926. * - `'uid'`: The UID of the message
  927. * - `'received'`: The date the message was received by the server
  928. * - `'headers'`: An associative array of mail headers, the keys are the header names, in lowercase
  929. *
  930. * And one or more of the following:
  931. *
  932. * - `'text'`: The plaintext body
  933. * - `'html'`: The HTML body
  934. * - `'attachment'`: An array of attachments, each containing:
  935. * - `'filename'`: The name of the file
  936. * - `'mimetype'`: The mimetype of the file
  937. * - `'data'`: The raw contents of the file
  938. * - `'inline'`: An array of inline files, each containing:
  939. * - `'filename'`: The name of the file
  940. * - `'mimetype'`: The mimetype of the file
  941. * - `'data'`: The raw contents of the file
  942. * - `'related'`: An associative array of related files, such as embedded images, with the key `'cid:{content-id}'` and an array value containing:
  943. * - `'mimetype'`: The mimetype of the file
  944. * - `'data'`: The raw contents of the file
  945. * - `'verified'`: If the message contents were verified via an S/MIME certificate - if not verified the smime.p7s will be listed as an attachment
  946. * - `'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
  947. *
  948. * All values in `headers`, `text` and `body` will have been decoded to
  949. * UTF-8. Files in the `attachment`, `inline` and `related` array will all
  950. * retain their original encodings.
  951. *
  952. * @param integer $uid The UID of the message to retrieve
  953. * @param boolean $convert_newlines If `\r\n` should be converted to `\n` in the `text` and `html` parts the message
  954. * @return array The parsed email message - see method description for details
  955. */
  956. public function fetchMessage($uid, $convert_newlines=FALSE)
  957. {
  958. $this->connect();
  959. if ($this->type == 'imap') {
  960. $response = $this->write('UID FETCH ' . $uid . ' (BODY[])');
  961. preg_match('#\{(\d+)\}$#', $response[0], $match);
  962. $message = '';
  963. foreach ($response as $i => $line) {
  964. if (!$i) { continue; }
  965. if (strlen($message) + strlen($line) + 2 > $match[1]) {
  966. $message .= substr($line . "\r\n", 0, $match[1] - strlen($message));
  967. } else {
  968. $message .= $line . "\r\n";
  969. }
  970. }
  971. $info = self::parseMessage($message, $convert_newlines);
  972. $info['uid'] = $uid;
  973. } elseif ($this->type == 'pop3') {
  974. $response = $this->write('RETR ' . $uid);
  975. array_shift($response);
  976. $response = join("\r\n", $response);
  977. $info = self::parseMessage($response, $convert_newlines);
  978. $info['uid'] = $uid;
  979. }
  980. return $info;
  981. }
  982. /**
  983. * Gets a list of messages from the server
  984. *
  985. * The structure of the returned array is:
  986. *
  987. * {{{
  988. * array(
  989. * (integer) {uid} => array(
  990. * 'uid' => (integer) {a unique identifier for this message on this server},
  991. * 'received' => (string) {date message was received},
  992. * 'size' => (integer) {size of message in bytes},
  993. * 'date' => (string) {date message was sent},
  994. * 'from' => (string) {the from header value},
  995. * 'subject' => (string) {the message subject},
  996. * 'message_id' => (string) {optional - the message-id header value, should be globally unique},
  997. * 'to' => (string) {optional - the to header value},
  998. * 'in_reply_to' => (string) {optional - the in-reply-to header value}
  999. * ), ...
  1000. * )
  1001. * }}}
  1002. *
  1003. * All values will have been decoded to UTF-8.
  1004. *
  1005. * @param integer $limit The number of messages to retrieve
  1006. * @param integer $page The page of messages to retrieve
  1007. * @return array A list of messages on the server - see method description for details
  1008. */
  1009. public function listMessages($limit=NULL, $page=NULL)
  1010. {
  1011. $this->connect();
  1012. if ($this->type == 'imap') {
  1013. if (!$limit) {
  1014. $start = 1;
  1015. $end = '*';
  1016. } else {
  1017. if (!$page) {
  1018. $page = 1;
  1019. }
  1020. $start = ($limit * ($page-1)) + 1;
  1021. $end = $start + $limit - 1;
  1022. }
  1023. $total_messages = 0;
  1024. $response = $this->write('STATUS "INBOX" (MESSAGES)');
  1025. foreach ($response as $line) {
  1026. if (preg_match('#^\s*\*\s+STATUS\s+"?INBOX"?\s+\((.*)\)$#', $line, $match)) {
  1027. $details = self::parseResponse($match[1], TRUE);
  1028. $total_messages = $details['messages'];
  1029. }
  1030. }
  1031. if ($start > $total_messages) {
  1032. return array();
  1033. }
  1034. if ($end > $total_messages) {
  1035. $end = $total_messages;
  1036. }
  1037. $output = array();
  1038. $response = $this->write('FETCH ' . $start . ':' . $end . ' (UID INTERNALDATE RFC822.SIZE ENVELOPE)');
  1039. foreach ($response as $line) {
  1040. if (preg_match('#^\s*\*\s+(\d+)\s+FETCH\s+\((.*)\)$#', $line, $match)) {
  1041. $details = self::parseResponse($match[2], TRUE);
  1042. $info = array();
  1043. $info['uid'] = $details['uid'];
  1044. $info['received'] = self::cleanDate($details['internaldate']);
  1045. $info['size'] = $details['rfc822.size'];
  1046. $envelope = $details['envelope'];
  1047. $info['date'] = $envelope[0] != 'NIL' ? $envelope[0] : '';
  1048. $info['from'] = self::joinEmails($envelope[2]);
  1049. if (preg_match('#=\?[^\?]+\?[QB]\?[^\?]+\?=#', $envelope[1])) {
  1050. do {
  1051. $last_subject = $envelope[1];
  1052. $envelope[1] = preg_replace('#(=\?([^\?]+)\?[QB]\?[^\?]+\?=) (\s*=\?\2)#', '\1\3', $envelope[1]);
  1053. } while ($envelope[1] != $last_subject);
  1054. $info['subject'] = self::decodeHeader($envelope[1]);
  1055. } else {
  1056. $info['subject'] = $envelope[1] == 'NIL' ? '' : self::decodeHeader($envelope[1]);
  1057. }
  1058. if ($envelope[9] != 'NIL') {
  1059. $info['message_id'] = $envelope[9];
  1060. }
  1061. if ($envelope[5] != 'NIL') {
  1062. $info['to'] = self::joinEmails($envelope[5]);
  1063. }
  1064. if ($envelope[8] != 'NIL') {
  1065. $info['in_reply_to'] = $envelope[8];
  1066. }
  1067. $output[$info['uid']] = $info;
  1068. }
  1069. }
  1070. } elseif ($this->type == 'pop3') {
  1071. if (!$limit) {
  1072. $start = 1;
  1073. $end = NULL;
  1074. } else {
  1075. if (!$page) {
  1076. $page = 1;
  1077. }
  1078. $start = ($limit * ($page-1)) + 1;
  1079. $end = $start + $limit - 1;
  1080. }
  1081. $total_messages = 0;
  1082. $response = $this->write('STAT', 1);
  1083. preg_match('#^\+OK\s+(\d+)\s+#', $response[0], $match);
  1084. $total_messages = $match[1];
  1085. if ($start > $total_messages) {
  1086. return array();
  1087. }
  1088. if ($end === NULL || $end > $total_messages) {
  1089. $end = $total_messages;
  1090. }
  1091. $sizes = array();
  1092. $response = $this->write('LIST');
  1093. array_shift($response);
  1094. foreach ($response as $line) {
  1095. preg_match('#^(\d+)\s+(\d+)$#', $line, $match);
  1096. $sizes[$match[1]] = $match[2];
  1097. }
  1098. $output = array();
  1099. for ($i = $start; $i <= $end; $i++) {
  1100. $response = $this->write('TOP ' . $i . ' 1');
  1101. array_shift($response);
  1102. $value = array_pop($response);
  1103. // Some servers add an extra blank line after the 1 requested line
  1104. if (trim($value) == '') {
  1105. array_pop($response);
  1106. }
  1107. $response = trim(join("\r\n", $response));
  1108. $headers = self::parseHeaders($response);
  1109. $output[$i] = array(
  1110. 'uid' => $i,
  1111. 'received' => self::cleanDate(preg_replace('#^.*;\s*([^;]+)$#', '\1', $headers['received'][0])),
  1112. 'size' => $sizes[$i],
  1113. 'date' => $headers['date'],
  1114. 'from' => self::joinEmails(array($headers['from'])),
  1115. 'subject' => isset($headers['subject']) ? $headers['subject'] : ''
  1116. );
  1117. if (isset($headers['message-id'])) {
  1118. $output[$i]['message_id'] = $headers['message-id'];
  1119. }
  1120. if (isset($headers['to'])) {
  1121. $output[$i]['to'] = self::joinEmails($headers['to']);
  1122. }
  1123. if (isset($headers['in-reply-to'])) {
  1124. $output[$i]['in_reply_to'] = $headers['in-reply-to'];
  1125. }
  1126. }
  1127. }
  1128. return $output;
  1129. }
  1130. /**
  1131. * Reads responses from the server
  1132. *
  1133. * @param integer|string $expect The expected number of lines of response or a regex of the last line
  1134. * @return array The lines of response from the server
  1135. */
  1136. private function read($expect=NULL)
  1137. {
  1138. $read = array($this->connection);
  1139. $write = NULL;
  1140. $except = NULL;
  1141. $response = array();
  1142. // PHP 5.2.0 to 5.2.5 has a bug on amd64 linux where stream_select()
  1143. // fails, so we have to fake it - http://bugs.php.net/bug.php?id=42682
  1144. static $broken_select = NULL;
  1145. if ($broken_select === NULL) {
  1146. $broken_select = strpos(php_uname('m'), '64') !== FALSE && fCore::checkVersion('5.2.0') && !fCore::checkVersion('5.2.6');
  1147. }
  1148. // Fixes an issue with stream_select throwing a warning on PHP 5.3 on Windows
  1149. if (fCore::checkOS('windows') && fCore::checkVersion('5.3.0')) {
  1150. $select = @stream_select($read, $write, $except, $this->timeout);
  1151. } elseif ($broken_select) {
  1152. $broken_select_buffer = NULL;
  1153. $start_time = microtime(TRUE);
  1154. $i = 0;
  1155. do {
  1156. if ($i) {
  1157. usleep(50000);
  1158. }
  1159. $char = fgetc($this->connection);
  1160. if ($char != "\x00" && $char !== FALSE) {
  1161. $broken_select_buffer = $char;
  1162. }
  1163. $i++;
  1164. } while ($broken_select_buffer === NULL && microtime(TRUE) - $start_time < $this->timeout);
  1165. $select = $broken_select_buffer !== NULL;
  1166. } else {
  1167. $select = stream_select($read, $write, $except, $this->timeout);
  1168. }
  1169. if ($select) {
  1170. while (!feof($this->connection)) {
  1171. $line = fgets($this->connection);
  1172. if ($line === FALSE) {
  1173. break;
  1174. }
  1175. $line = substr($line, 0, -2);
  1176. // When we fake select, we have to handle what we've retrieved
  1177. if ($broken_select && $broken_select_buffer !== NULL) {
  1178. $line = $broken_select_buffer . $line;
  1179. $broken_select_buffer = NULL;
  1180. }
  1181. $response[] = $line;
  1182. // Automatically stop at the termination octet or a bad response
  1183. if ($this->type == 'pop3' && ($line == '.' || (count($response) == 1 && $response[0][0] == '-'))) {
  1184. break;
  1185. }
  1186. if ($expect !== NULL) {
  1187. $matched_number = is_int($expect) && sizeof($response) == $expect;
  1188. $matched_regex = is_string($expect) && preg_match($expect, $line);
  1189. if ($matched_number || $matched_regex) {
  1190. break;
  1191. }
  1192. }
  1193. }
  1194. }
  1195. if (fCore::getDebug($this->debug)) {
  1196. fCore::debug("Received:\n" . join("\r\n", $response), $this->debug);
  1197. }
  1198. if ($this->type == 'pop3') {
  1199. // Remove the termination octet
  1200. if ($response && $response[sizeof($response)-1] == '.') {
  1201. $response = array_slice($response, 0, -1);
  1202. }
  1203. // Remove byte-stuffing
  1204. $lines = count($response);
  1205. for ($i = 0; $i < $lines; $i++) {
  1206. if (strlen($response[$i]) && $response[$i][0] == '.') {
  1207. $response[$i] = substr($response[$i], 1);
  1208. }
  1209. }
  1210. }
  1211. return $response;
  1212. }
  1213. /**
  1214. * Sends commands to the IMAP or POP3 server
  1215. *
  1216. * @param string $command The command to send
  1217. * @param integer $expected The number of lines or regex expected for a POP3 command
  1218. * @return array The response from the server
  1219. */
  1220. private function write($command, $expected=NULL)
  1221. {
  1222. if (!$this->connection) {
  1223. throw new fProgrammerException('Unable to send data since the connection has already been closed');
  1224. }
  1225. if ($this->type == 'imap') {
  1226. $identifier = 'a' . str_pad($this->command_num++, 4, '0', STR_PAD_LEFT);
  1227. $command = $identifier . ' ' . $command;
  1228. }
  1229. if (substr($command, -2) != "\r\n") {
  1230. $command .= "\r\n";
  1231. }
  1232. if (fCore::getDebug($this->debug)) {
  1233. fCore::debug("Sending:\n" . trim($command), $this->debug);
  1234. }
  1235. $res = fwrite($this->connection, $command);
  1236. if ($res === FALSE) {
  1237. throw new fConnectivityException(
  1238. 'Unable to write data to %1$s server %2$s on port %3$s',
  1239. strtoupper($this->type),
  1240. $this->host,
  1241. $this->port
  1242. );
  1243. }
  1244. if ($this->type == 'imap') {
  1245. return $this->read('#^' . $identifier . '#');
  1246. } elseif ($this->type == 'pop3') {
  1247. return $this->read($expected);
  1248. }
  1249. }
  1250. }
  1251. /**
  1252. * Copyright (c) 2010-2011 Will Bond <will@flourishlib.com>
  1253. *
  1254. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1255. * of this software and associated documentation files (the "Software"), to deal
  1256. * in the Software without restriction, including without limitation the rights
  1257. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1258. * copies of the Software, and to permit persons to whom the Software is
  1259. * furnished to do so, subject to the following conditions:
  1260. *
  1261. * The above copyright notice and this permission notice shall be included in
  1262. * all copies or substantial portions of the Software.
  1263. *
  1264. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1265. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1266. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1267. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1268. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1269. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1270. * THE SOFTWARE.
  1271. */