PageRenderTime 74ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/fMailbox.php

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