PageRenderTime 61ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

/imp-h3-4.3.10/lib/IMAP/Client.php

#
PHP | 666 lines | 379 code | 68 blank | 219 comment | 83 complexity | a4d40a855bcf04b38062cfe4ef5e41fd MD5 | raw file
Possible License(s): AGPL-1.0
  1. <?php
  2. define('IMP_IMAPCLIENT_TAGGED', 0);
  3. define('IMP_IMAPCLIENT_UNTAGGED', 1);
  4. define('IMP_IMAPCLIENT_CONTINUATION', 2);
  5. /**
  6. * The IMP_IMAPClient:: class enables connection to an IMAP server through
  7. * built-in PHP functions.
  8. *
  9. * TODO: This should eventually be moved to Horde 4.0/framework.
  10. *
  11. * $Horde: imp/lib/IMAP/Client.php,v 1.21.2.36 2009/08/05 08:36:37 slusarz Exp $
  12. *
  13. * Copyright 2005-2009 The Horde Project (http://www.horde.org/)
  14. *
  15. * Based on code from:
  16. * + auth.php (1.49)
  17. * + imap_general.php (1.212)
  18. * + strings.php (1.184.2.35)
  19. * from the Squirrelmail project.
  20. * Copyright 1999-2005 The SquirrelMail Project Team
  21. *
  22. * See the enclosed file COPYING for license information (GPL). If you
  23. * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
  24. *
  25. * @author Michael Slusarz <slusarz@horde.org>
  26. * @since IMP 4.1
  27. * @package IMP
  28. */
  29. class IMP_IMAPClient {
  30. /**
  31. * The list of capabilities of the IMAP server.
  32. *
  33. * @var array
  34. */
  35. var $_capability = null;
  36. /**
  37. * The hostname of the IMAP server to connect to.
  38. *
  39. * @var string
  40. */
  41. var $_host;
  42. /**
  43. * The namespace information.
  44. *
  45. * @var array
  46. */
  47. var $_namespace = null;
  48. /**
  49. * The port number of the IMAP server to connect to.
  50. *
  51. * @var string
  52. */
  53. var $_port;
  54. /**
  55. * The unique ID to use when making an IMAP query.
  56. *
  57. * @var integer
  58. */
  59. var $_sessionid = 1;
  60. /**
  61. * The currently active tag.
  62. *
  63. * @var string
  64. */
  65. var $_currtag = null;
  66. /**
  67. * The socket connection to the IMAP server.
  68. *
  69. * @var resource
  70. */
  71. var $_stream;
  72. /**
  73. * Are we using SSL to connect to the IMAP server?
  74. *
  75. * @var string
  76. */
  77. var $_usessl = false;
  78. /**
  79. * Are we using TLS to connect to the IMAP server?
  80. *
  81. * @var string
  82. */
  83. var $_usetls = false;
  84. /**
  85. * Constructor.
  86. *
  87. * @param string $host The address/hostname of the IMAP server.
  88. * @param string $port The port to connect to on the IMAP server.
  89. * @param string $protocol The protocol string (See, e.g., servers.php).
  90. */
  91. function IMP_IMAPClient($host, $port, $protocol)
  92. {
  93. $this->_host = $host;
  94. $this->_port = $port;
  95. /* Split apart protocol string to discover if we need to use either
  96. * SSL or TLS. */
  97. $tmp = explode('/', strtolower($protocol));
  98. if (in_array('tls', $tmp)) {
  99. $this->_usetls = true;
  100. } elseif (in_array('ssl', $tmp)) {
  101. $this->_usessl = true;
  102. }
  103. }
  104. /**
  105. * Are we using TLS to connect and is it supported?
  106. *
  107. * @return mixed Returns true if TLS is being used to connect, false if
  108. * is not, and PEAR_Error if we are attempting to use TLS
  109. * and this version of PHP doesn't support it.
  110. */
  111. function useTLS()
  112. {
  113. if ($this->_usetls) {
  114. /* There is no way in PHP 4 to open a TLS connection to a
  115. * non-secured port. See http://bugs.php.net/bug.php?id=26040 */
  116. if (!function_exists('stream_socket_enable_crypto')) {
  117. return PEAR::raiseError(_("To use a TLS connection, you must be running a version of PHP 5.1.0 or higher."), 'horde.error');
  118. }
  119. }
  120. return $this->_usetls;
  121. }
  122. /**
  123. * Generates a new IMAP session ID by incrementing the last one used.
  124. *
  125. * @access private
  126. *
  127. * @return string IMAP session id of the form 'A000'.
  128. */
  129. function _generateSid()
  130. {
  131. return sprintf("A%03d", $this->_sessionid++);
  132. }
  133. /**
  134. * Perform a command on the IMAP server.
  135. *
  136. * @access private
  137. *
  138. * @param string $query The IMAP command to execute.
  139. *
  140. * @return stdClass Returns PEAR_Error on error. On success, returns
  141. * a stdClass object with the following elements:
  142. * <pre>
  143. * 'message' - The response message
  144. * 'response' - The response code
  145. * 'type' - Either IMP_IMAPCLIENT_TAGGED, IMP_IMAPCLIENT_UNTAGGED, or
  146. * IMP_IMAPCLIENT_CONTINUATION
  147. * </pre>
  148. */
  149. function _runCommand($query)
  150. {
  151. if (!$this->_currtag) {
  152. $this->_currtag = $this->_generateSid();
  153. $query = $this->_currtag . ' ' . $query;
  154. }
  155. fwrite($this->_stream, $query . "\r\n");
  156. $ob = $this->_parseLine();
  157. if (is_a($ob, 'PEAR_Error')) {
  158. $this->_currtag = null;
  159. return $ob;
  160. }
  161. switch ($ob->response) {
  162. case 'OK':
  163. break;
  164. case 'NO':
  165. /* Ignore this error from M$ exchange, it is not fatal (aka
  166. * bug). */
  167. if (strstr($ob->message, 'command resulted in') === false) {
  168. $this->_currtag = null;
  169. return PEAR::raiseError(sprintf(_("Could not complete request. Reason Given: %s"), $ob->message), 'horde.error', null, null, $ob->response);
  170. }
  171. break;
  172. case 'BAD':
  173. $this->_currtag = null;
  174. return PEAR::raiseError(sprintf(_("Bad or malformed request. Server Responded: %s"), $ob->message), 'horde.error', null, null, $ob->response);
  175. break;
  176. case 'BYE':
  177. $this->_currtag = null;
  178. return PEAR::raiseError(sprintf(_("IMAP Server closed the connection. Server Responded: %s"), $ob->message), 'horde.error', null, null, $ob->response);
  179. break;
  180. default:
  181. $this->_currtag = null;
  182. return PEAR::raiseError(sprintf(_("Unknown IMAP response from the server. Server Responded: %s"), $ob->message), 'horde.error', null, null, $ob->response);
  183. break;
  184. }
  185. if ($ob->type != IMP_IMAPCLIENT_CONTINUATION) {
  186. $this->_currtag = null;
  187. }
  188. return $ob;
  189. }
  190. /**
  191. * TODO
  192. *
  193. * @access private
  194. *
  195. * @return stdClass See _runCommand().
  196. */
  197. function _parseLine()
  198. {
  199. $ob = new stdClass;
  200. $read = explode(' ', trim(fgets($this->_stream)), 2);
  201. switch ($read[0]) {
  202. /* Continuation response. */
  203. case '+':
  204. $ob->message = isset($read[1]) ? trim($read[1]) : '';
  205. $ob->response = 'OK';
  206. $ob->type = IMP_IMAPCLIENT_CONTINUATION;
  207. break;
  208. /* Untagged response. */
  209. case '*':
  210. $tmp = explode(' ', $read[1], 2);
  211. $ob->response = trim($tmp[0]);
  212. if (in_array($ob->response, array('OK', 'NO', 'BAD', 'PREAUTH', 'BYE'))) {
  213. $ob->message = trim($tmp[1]);
  214. } else {
  215. $ob->response = 'OK';
  216. $ob->message = $read[1];
  217. }
  218. $ob->type = IMP_IMAPCLIENT_UNTAGGED;
  219. $ob2 = $this->_parseLine();
  220. if ($ob2->response != 'OK') {
  221. $ob = $ob2;
  222. } elseif ($ob2->type == IMP_IMAPCLIENT_UNTAGGED) {
  223. $ob->message .= "\n" . $ob2->message;
  224. } else {
  225. $ob->response = $ob2->response;
  226. }
  227. break;
  228. /* Tagged response. */
  229. default:
  230. $tmp = explode(' ', $read[1], 2);
  231. $ob->type = IMP_IMAPCLIENT_TAGGED;
  232. if ($this->_currtag && ($read[0] == $this->_currtag)) {
  233. $ob->message = trim($tmp[1]);
  234. $ob->response = trim($tmp[0]);
  235. } else {
  236. $ob->message = $read[0];
  237. $ob->response = '';
  238. }
  239. break;
  240. }
  241. return $ob;
  242. }
  243. /**
  244. * Connects to the IMAP server.
  245. *
  246. * @access private
  247. *
  248. * @return mixed Returns true on success, PEAR_Error on error.
  249. */
  250. function _createStream()
  251. {
  252. if (($this->_usessl || $this->_usetls) &&
  253. !Util::extensionExists('openssl')) {
  254. return PEAR::raiseError(_("If using SSL or TLS, you must have the PHP openssl extension loaded."), 'horde.error');
  255. }
  256. if ($res = $this->useTLS()) {
  257. if (is_a($res, 'PEAR_Error')) {
  258. return $res;
  259. } else {
  260. $this->_host = 'tcp://' . $this->_host . ':' . $this->_port;
  261. }
  262. }
  263. if ($this->_usessl) {
  264. $this->_host = 'ssl://' . $this->_host;
  265. }
  266. $error_number = $error_string = '';
  267. $timeout = 10;
  268. if ($this->_usetls) {
  269. $this->_stream = stream_socket_client($this->_host, $error_number, $error_string, $timeout);
  270. if (!$this->_stream) {
  271. return PEAR::raiseError(sprintf(_("Error connecting to IMAP server: [%s] %s."), $error_number, $error_string), 'horde.error');
  272. }
  273. /* Disregard any server information returned. */
  274. fgets($this->_stream);
  275. /* Send the STARTTLS command. */
  276. $res = $this->_runCommand('STARTTLS');
  277. /* Switch over to a TLS connection. */
  278. if (!is_a($res, 'PEAR_Error')) {
  279. $res = stream_socket_enable_crypto($this->_stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
  280. }
  281. if (!$res || is_a($res, 'PEAR_Error')) {
  282. $this->logout();
  283. return PEAR::raiseError(_("Could not open secure connection to the IMAP server."), 'horde.error');
  284. }
  285. } else {
  286. $this->_stream = fsockopen($this->_host, $this->_port, $error_number, $error_string, $timeout);
  287. if (!$this->_stream) {
  288. return PEAR::raiseError(sprintf(_("Error connecting to IMAP server: [%s] %s."), $error_number, $error_string), 'horde.error');
  289. }
  290. /* Disregard server information. */
  291. fgets($this->_stream);
  292. }
  293. register_shutdown_function(array(&$this, 'logout'));
  294. }
  295. /**
  296. * Log the user into the IMAP server.
  297. *
  298. * @param string $username Username.
  299. * @param string $password Encrypted password.
  300. *
  301. * @return mixed True on success, PEAR_Error on error.
  302. */
  303. function login($username, $password)
  304. {
  305. $res = $this->_createStream();
  306. if (is_a($res, 'PEAR_Error')) {
  307. Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_ERR);
  308. return $res;
  309. }
  310. $imap_auth_mech = array();
  311. /* Use md5 authentication, if available. But no need to use special
  312. * authentication if we are already using an encrypted connection. */
  313. $auth_methods = $this->queryCapability('AUTH');
  314. if (!$this->_usessl && !$this->_usetls && !empty($auth_methods)) {
  315. if (in_array('CRAM-MD5', $auth_methods)) {
  316. $imap_auth_mech[] = 'cram-md5';
  317. }
  318. if (in_array('DIGEST-MD5', $auth_methods)) {
  319. $imap_auth_mech[] = 'digest-md5';
  320. }
  321. }
  322. /* Next, try 'PLAIN' authentication. */
  323. if (!empty($auth_methods) && in_array('PLAIN', $auth_methods)) {
  324. $imap_auth_mech[] = 'plain';
  325. }
  326. /* Fall back to 'LOGIN' if available. */
  327. if (!$this->queryCapability('LOGINDISABLED')) {
  328. $imap_auth_mech[] = 'login';
  329. }
  330. if (empty($imap_auth_mech)) {
  331. return PEAR::raiseError(_("No supported IMAP authentication method could be found."), 'horde.error');
  332. }
  333. foreach ($imap_auth_mech as $method) {
  334. $res = $this->_login($username, $password, $method);
  335. if (!is_a($res, 'PEAR_Error')) {
  336. return true;
  337. } else {
  338. Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_WARNING);
  339. }
  340. }
  341. return $res;
  342. }
  343. /**
  344. * Log the user into the IMAP server.
  345. *
  346. * @access private
  347. *
  348. * @param string $username Username.
  349. * @param string $password Encrypted password.
  350. * @param string $method IMAP login method.
  351. *
  352. * @return mixed True on success, PEAR_Error on error.
  353. */
  354. function _login($username, $password, $method)
  355. {
  356. switch ($method) {
  357. case 'cram-md5':
  358. case 'digest-md5':
  359. /* If we don't have Auth_SASL package install, return error. */
  360. if (!@include_once 'Auth/SASL.php') {
  361. return PEAR::raiseError(_("CRAM-MD5 or DIGEST-MD5 requires PEAR's Auth_SASL package to be installed."), 'horde.error');
  362. }
  363. $res = $this->_runCommand('AUTHENTICATE ' . $method);
  364. if (is_a($res, 'PEAR_Error')) {
  365. return $res;
  366. }
  367. if ($method == 'cram-md5') {
  368. $auth_sasl = Auth_SASL::factory('crammd5');
  369. $response = $auth_sasl->getResponse($username, $password, base64_decode($res->message));
  370. $read = $this->_runCommand(base64_encode($response));
  371. } elseif ($method == 'digest-md5') {
  372. $auth_sasl = Auth_SASL::factory('digestmd5');
  373. $response = $auth_sasl->getResponse($username, $password, base64_decode($res->message), $this->_host, 'imap');
  374. $res = $this->_runCommand(base64_encode($response));
  375. if (is_a($res, 'PEAR_Error')) {
  376. return $res;
  377. }
  378. $response = base64_decode($res->message);
  379. if (strpos($response, 'rspauth=') === false) {
  380. return PEAR::raiseError(_("Unexpected response from server to Digest-MD5 response."), 'horde.error');
  381. }
  382. $read = $this->_runCommand('');
  383. } else {
  384. return PEAR::raiseError(_("The IMAP server does not appear to support the authentication method selected. Please contact your system administrator."), 'horde.error');
  385. }
  386. break;
  387. case 'login':
  388. /* We should use a literal string to send the username, but some
  389. * IMAP servers don't support a literal string request inside of a
  390. * literal string. Thus, use a quoted string for the username
  391. * (which should probably be OK since it is very unlikely a
  392. * username will include a double-quote character). */
  393. $read = $this->_runCommand("LOGIN \"$username\" {" . strlen($password) . "}");
  394. if (!is_a($read, 'PEAR_Error') &&
  395. ($read->type == IMP_IMAPCLIENT_CONTINUATION)) {
  396. $read = $this->_runCommand($password);
  397. }
  398. break;
  399. case 'plain':
  400. $sasl = $this->queryCapability('SASL-IR');
  401. $auth = base64_encode("$username\0$username\0$password");
  402. if ($sasl) {
  403. // IMAP Extension for SASL Initial Client Response
  404. // <draft-siemborski-imap-sasl-initial-response-01b.txt>
  405. $read = $this->_runCommand("AUTHENTICATE PLAIN $auth");
  406. } else {
  407. $read = $this->_runCommand("AUTHENTICATE PLAIN");
  408. if (!is_a($read, 'PEAR_Error') &&
  409. ($read->type == IMP_IMAPCLIENT_CONTINUATION)) {
  410. $read = $this->_runCommand($auth);
  411. } else {
  412. return PEAR::raiseError(_("Unexpected response from server to AUTHENTICATE command."), 'horde.error');
  413. }
  414. }
  415. break;
  416. }
  417. if (is_a($read, 'PEAR_Error')) {
  418. return $read;
  419. }
  420. /* Check for failed login. */
  421. if ($read->response != 'OK') {
  422. $message = !empty($read->message) ? htmlspecialchars($read->message) : _("No message returned.");
  423. switch ($read->response) {
  424. case 'NO':
  425. return PEAR::raiseError(sprintf(_("Bad login name or password."), $message), 'horde.error');
  426. case 'BAD':
  427. default:
  428. return PEAR::raiseError(sprintf(_("Bad request: %s"), $message), 'horde.error');
  429. }
  430. }
  431. return true;
  432. }
  433. /**
  434. * Log out of the IMAP session.
  435. */
  436. function logout()
  437. {
  438. if (!empty($this->_stream)) {
  439. $this->_runCommand('LOGOUT');
  440. fclose($this->_stream);
  441. }
  442. }
  443. /**
  444. * Get the CAPABILITY string from the IMAP server.
  445. *
  446. * @access private
  447. */
  448. function _capability()
  449. {
  450. if ($this->_capability !== null) {
  451. return;
  452. }
  453. $this->_capability = array();
  454. $read = $this->_runCommand('CAPABILITY');
  455. if (is_a($read, 'PEAR_Error')) {
  456. Horde::logMessage($read, __FILE__, __LINE__, PEAR_LOG_ERR);
  457. return;
  458. }
  459. $c = explode(' ', $read->message);
  460. for ($i = 2; $i < count($c); $i++) {
  461. $cap_list = explode('=', $c[$i]);
  462. if (isset($cap_list[1])) {
  463. if (!isset($this->_capability[$cap_list[0]]) ||
  464. !is_array($this->_capability[$cap_list[0]])) {
  465. $this->_capability[$cap_list[0]] = array();
  466. }
  467. $this->_capability[$cap_list[0]][] = $cap_list[1];
  468. } elseif (!isset($this->_capability[$cap_list[0]])) {
  469. $this->_capability[$cap_list[0]] = true;
  470. }
  471. }
  472. }
  473. /**
  474. * Returns whether the IMAP server supports the given capability.
  475. *
  476. * @param string $capability The capability string to query. If null,
  477. * returns the entire capability array.
  478. *
  479. * @param mixed True if the server supports the queried capability,
  480. * false if it doesn't, or an array if the capability can
  481. * contain multiple values.
  482. */
  483. function queryCapability($capability)
  484. {
  485. $this->_capability();
  486. return ($capability === null) ? $this->_capability : (isset($this->_capability[$capability]) ? $this->_capability[$capability] : false);
  487. }
  488. /**
  489. * Get the NAMESPACE information from the IMAP server.
  490. *
  491. * @param array $additional If the server supports namespaces, any
  492. * additional namespaces to add to the
  493. * namespace list that are not broadcast by
  494. * the server.
  495. *
  496. * @return array An array with the following format:
  497. * <pre>
  498. * Array
  499. * (
  500. * [foo1] => Array
  501. * (
  502. * [name] => (string)
  503. * [delimiter] => (string)
  504. * [type] => [personal|other|shared] (string)
  505. * [hidden] => (boolean)
  506. * )
  507. *
  508. * [foo2] => Array
  509. * (
  510. * ...
  511. * )
  512. * )
  513. * </pre>
  514. * Returns PEAR_Error object on error.
  515. */
  516. function getNamespace($additional = array())
  517. {
  518. if ($this->_namespace !== null) {
  519. return $this->_namespace;
  520. }
  521. $namespace_array = array(
  522. 1 => 'personal',
  523. 2 => 'other',
  524. 3 => 'shared'
  525. );
  526. if ($this->queryCapability('NAMESPACE')) {
  527. /* According to RFC 2342, response from NAMESPACE command is:
  528. * * NAMESPACE (PERSONAL NAMESPACES) (OTHER_USERS NAMESPACE) (SHARED NAMESPACES)
  529. */
  530. $read = $this->_runCommand('NAMESPACE');
  531. if (is_a($read, 'PEAR_Error')) {
  532. Horde::logMessage($read, __FILE__, __LINE__, PEAR_LOG_ERR);
  533. return $read;
  534. }
  535. if (($read->type == IMP_IMAPCLIENT_UNTAGGED) &&
  536. eregi('NAMESPACE +(\\( *\\(.+\\) *\\)|NIL) +(\\( *\\(.+\\) *\\)|NIL) +(\\( *\\(.+\\) *\\)|NIL)', $read->message, $data)) {
  537. for ($i = 1; $i <= 3; $i++) {
  538. if ($data[$i] == 'NIL') {
  539. continue;
  540. }
  541. $pna = explode(')(', $data[$i]);
  542. while (list($k, $v) = each($pna)) {
  543. $lst = explode('"', $v);
  544. $delimiter = (isset($lst[3])) ? $lst[3] : '';
  545. $this->_namespace[$lst[1]] = array('name' => $lst[1], 'delimiter' => $delimiter, 'type' => $namespace_array[$i], 'hidden' => false);
  546. }
  547. }
  548. }
  549. foreach ($additional as $val) {
  550. /* Skip namespaces if we have already auto-detected them.
  551. * Also, hidden namespaces cannot be empty. */
  552. $val = trim($val);
  553. if (empty($val) || isset($this->_namespace[$val])) {
  554. continue;
  555. }
  556. $read = $this->_runCommand('LIST "" "' . $val . '"');
  557. if (is_a($read, 'PEAR_Error')) {
  558. Horde::logMessage($read, __FILE__, __LINE__, PEAR_LOG_ERR);
  559. return $read;
  560. }
  561. if (($read->type == IMP_IMAPCLIENT_UNTAGGED) &&
  562. preg_match("/^LIST \(.*\) \"(.*)\" \"?(.*?)\"?\s*$/", $read->message, $data) &&
  563. ($data[2] == $val)) {
  564. $this->_namespace[$val] = array('name' => $val, 'delimiter' => $data[1], 'type' => $namespace_array[3], 'hidden' => true);
  565. }
  566. }
  567. }
  568. if (empty($this->_namespace)) {
  569. $res = $this->_runCommand('LIST "" ""');
  570. if (is_a($res, 'PEAR_Error')) {
  571. Horde::logMessage($res, __FILE__, __LINE__, PEAR_LOG_ERR);
  572. return $res;
  573. }
  574. $quote_position = strpos($res->message, '"');
  575. $this->_namespace[''] = array('name' => '', 'delimiter' => substr($res->message, $quote_position + 1 , 1), 'type' => $namespace_array[1], 'hidden' => false);
  576. }
  577. return $this->_namespace;
  578. }
  579. /**
  580. * Determines whether the IMAP search command supports the optional
  581. * charset provided.
  582. *
  583. * @param string $charset The character set to test.
  584. *
  585. * @return boolean True if the IMAP search command supports the charset.
  586. */
  587. function searchCharset($charset)
  588. {
  589. $read = $this->_runCommand('SELECT INBOX');
  590. if (!is_a($read, 'PEAR_Error')) {
  591. $read = $this->_runCommand('SEARCH CHARSET ' . $charset . ' TEXT "charsettest" 1');
  592. }
  593. return !is_a($read, 'PEAR_Error');
  594. }
  595. }