Sieve.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <?php
  2. // +-----------------------------------------------------------------------+
  3. // | Copyright (c) 2002-2003, Richard Heyes |
  4. // | All rights reserved. |
  5. // | |
  6. // | Redistribution and use in source and binary forms, with or without |
  7. // | modification, are permitted provided that the following conditions |
  8. // | are met: |
  9. // | |
  10. // | o Redistributions of source code must retain the above copyright |
  11. // | notice, this list of conditions and the following disclaimer. |
  12. // | o Redistributions in binary form must reproduce the above copyright |
  13. // | notice, this list of conditions and the following disclaimer in the |
  14. // | documentation and/or other materials provided with the distribution.|
  15. // | o The names of the authors may not be used to endorse or promote |
  16. // | products derived from this software without specific prior written |
  17. // | permission. |
  18. // | |
  19. // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
  20. // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
  21. // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
  22. // | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
  23. // | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
  24. // | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
  25. // | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
  26. // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
  27. // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
  28. // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
  29. // | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
  30. // | |
  31. // +-----------------------------------------------------------------------+
  32. // | Author: Richard Heyes <richard@phpguru.org> |
  33. // +-----------------------------------------------------------------------+
  34. require_once('Net/Socket.php');
  35. require_once('Auth/SASL.php');
  36. /**
  37. * TODO
  38. *
  39. * o hasExtension()
  40. * o getExtensions()
  41. * o supportsAuthMech()
  42. */
  43. /**
  44. * Disconnected state
  45. * @const NET_SIEVE_STATE_DISCONNECTED
  46. */
  47. define('NET_SIEVE_STATE_DISCONNECTED', 1, true);
  48. /**
  49. * Authorisation state
  50. * @const NET_SIEVE_STATE_AUTHORISATION
  51. */
  52. define('NET_SIEVE_STATE_AUTHORISATION', 2, true);
  53. /**
  54. * Transaction state
  55. * @const NET_SIEVE_STATE_TRANSACTION
  56. */
  57. define('NET_SIEVE_STATE_TRANSACTION', 3, true);
  58. /**
  59. * A class for talking to the timsieved server which
  60. * comes with Cyrus IMAP. Does not support the HAVESPACE
  61. * command which appears to be broken (Cyrus 2.0.16).
  62. *
  63. * @author Richard Heyes <richard@php.net>
  64. * @access public
  65. * @version 0.8
  66. * @package Net_Sieve
  67. */
  68. class Net_Sieve
  69. {
  70. /**
  71. * The socket object
  72. * @var object
  73. */
  74. var $_sock;
  75. /**
  76. * Info about the connect
  77. * @var array
  78. */
  79. var $_data;
  80. /**
  81. * Current state of the connection
  82. * @var integer
  83. */
  84. var $_state;
  85. /**
  86. * Constructor error is any
  87. * @var object
  88. */
  89. var $_error;
  90. /**
  91. * Constructor
  92. * Sets up the object, connects to the server and logs in. stores
  93. * any generated error in $this->_error, which can be retrieved
  94. * using the getError() method.
  95. *
  96. * @access public
  97. * @param string $user Login username
  98. * @param string $pass Login password
  99. * @param string $host Hostname of server
  100. * @param string $port Port of server
  101. * @param string $logintype Type of login to perform
  102. * @param string $euser Effective User (if $user=admin, login as $euser)
  103. */
  104. function Net_Sieve($user, $pass, $host = 'localhost', $port = 2000, $logintype = 'PLAIN', $euser = '')
  105. {
  106. $this->_state = NET_SIEVE_STATE_DISCONNECTED;
  107. $this->_data['user'] = $user;
  108. $this->_data['pass'] = $pass;
  109. $this->_data['host'] = $host;
  110. $this->_data['port'] = $port;
  111. $this->_data['euser'] = $euser;
  112. $this->_sock = &new Net_Socket();
  113. if (PEAR::isError($res = $this->_connect($host, $port))) {
  114. $this->_error = $res;
  115. return;
  116. }
  117. if (PEAR::isError($res = $this->_login($user, $pass, $logintype, $euser))) {
  118. $this->_error = $res;
  119. }
  120. }
  121. /**
  122. * Returns an indexed array of scripts currently
  123. * on the server
  124. *
  125. * @access public
  126. * @return mixed Indexed array of scriptnames or PEAR_Error on failure
  127. */
  128. function listScripts()
  129. {
  130. if (is_array($scripts = $this->_cmdListScripts())) {
  131. $this->_active = $scripts[1];
  132. return $scripts[0];
  133. } else {
  134. return $scripts;
  135. }
  136. }
  137. /**
  138. * Returns the active script
  139. *
  140. * @access public
  141. * @return mixed The active scriptname or PEAR_Error on failure
  142. */
  143. function getActive()
  144. {
  145. if (!empty($this->_active)) {
  146. return $this->_active;
  147. } elseif (is_array($scripts = $this->_cmdListScripts())) {
  148. $this->_active = $scripts[1];
  149. return $scripts[1];
  150. }
  151. }
  152. /**
  153. * Sets the active script
  154. *
  155. * @access public
  156. * @param string $scriptname The name of the script to be set as active
  157. * @return mixed true on success, PEAR_Error on failure
  158. */
  159. function setActive($scriptname)
  160. {
  161. return $this->_cmdSetActive($scriptname);
  162. }
  163. /**
  164. * Retrieves a script
  165. *
  166. * @access public
  167. * @param string $scriptname The name of the script to be retrieved
  168. * @return mixed The script on success, PEAR_Error on failure
  169. */
  170. function getScript($scriptname)
  171. {
  172. return $this->_cmdGetScript($scriptname);
  173. }
  174. /**
  175. * Adds a script to the server
  176. *
  177. * @access public
  178. * @param string $scriptname Name of the script
  179. * @param string $script The script
  180. * @param bool $makeactive Whether to make this the active script
  181. * @return mixed true on success, PEAR_Error on failure
  182. */
  183. function installScript($scriptname, $script, $makeactive = false)
  184. {
  185. if (PEAR::isError($res = $this->_cmdPutScript($scriptname, $script))) {
  186. return $res;
  187. } elseif ($makeactive) {
  188. return $this->_cmdSetActive($scriptname);
  189. } else {
  190. return true;
  191. }
  192. }
  193. /**
  194. * Removes a script from the server
  195. *
  196. * @access public
  197. * @param string $scriptname Name of the script
  198. * @return mixed True on success, PEAR_Error on failure
  199. */
  200. function removeScript($scriptname)
  201. {
  202. return $this->_cmdDeleteScript($scriptname);
  203. }
  204. /**
  205. * Returns any error that may have been generated in the
  206. * constructor
  207. *
  208. * @access public
  209. * @return mixed False if no error, PEAR_Error otherwise
  210. */
  211. function getError()
  212. {
  213. return PEAR::isError($this->_error) ? $this->_error : false;
  214. }
  215. /**
  216. * Handles connecting to the server and checking the
  217. * response is valid.
  218. *
  219. * @access private
  220. * @param string $host Hostname of server
  221. * @param string $port Port of server
  222. * @return mixed True on success, PEAR_Error otherwise
  223. */
  224. function _connect($host, $port)
  225. {
  226. if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
  227. if (PEAR::isError($res = $this->_sock->connect($host, $port, null, 5))) {
  228. return $res;
  229. }
  230. // Get logon greeting/capability and parse
  231. if(!PEAR::isError($res = $this->_getResponse())) {
  232. $this->_parseCapability($res);
  233. $this->_state = NET_SIEVE_STATE_AUTHORISATION;
  234. if (!isset($this->_capability['sasl'])) {
  235. return PEAR::raiseError('No authentication mechanisms available.');
  236. }
  237. return true;
  238. } else {
  239. return PEAR::raiseError('Failed to connect, server said: ' . $res->getMessage());
  240. }
  241. } else {
  242. return PEAR::raiseError('Not currently in DISCONNECTED state');
  243. }
  244. }
  245. /**
  246. * Logs into server.
  247. *
  248. * @access private
  249. * @param string $user Login username
  250. * @param string $pass Login password
  251. * @param string $logintype Type of login method to use
  252. * @param string $euser Effective UID (perform on behalf of $euser)
  253. * @return mixed True on success, PEAR_Error otherwise
  254. */
  255. function _login($user, $pass, $logintype = 'PLAIN', $euser = '')
  256. {
  257. if (NET_SIEVE_STATE_AUTHORISATION != $this->_state) {
  258. return PEAR::raiseError('Not currently in AUTHORISATION state');
  259. }
  260. if (!in_array($logintype, $this->_capability['sasl'])) {
  261. return PEAR::raiseError(sprintf('Authentication mechanism %s not supported by this server.', $logintype));
  262. }
  263. $sasl = &Auth_SASL::factory($logintype);
  264. if (PEAR::isError($sasl)) {
  265. return $sasl;
  266. }
  267. switch ($logintype) {
  268. case 'PLAIN':
  269. $this->_sendCmd(sprintf('AUTHENTICATE "PLAIN" "%s"',
  270. base64_encode($sasl->getResponse($user, $pass, $euser))));
  271. break;
  272. case 'LOGIN':
  273. $this->_sendCmd('AUTHENTICATE "LOGIN"');
  274. $this->_sendCmd('"' . base64_encode($user) . '"');
  275. $this->_sendCmd('"' . base64_encode($pass) . '"');
  276. break;
  277. default:
  278. return PEAR::raiseError(sprintf('Authentication mechanism %s not supported by this client.', $logintype));
  279. }
  280. if (!PEAR::isError($res = $this->_getResponse())) {
  281. $this->_state = NET_SIEVE_STATE_TRANSACTION;
  282. return true;
  283. } else {
  284. return $res;
  285. }
  286. }
  287. /**
  288. * Removes a script from the server
  289. *
  290. * @access private
  291. * @param string $scriptname Name of the script to delete
  292. * @return mixed True on success, PEAR_Error otherwise
  293. */
  294. function _cmdDeleteScript($scriptname)
  295. {
  296. if (NET_SIEVE_STATE_TRANSACTION === $this->_state) {
  297. $this->_sendCmd(sprintf('DELETESCRIPT "%s"', $scriptname));
  298. if (PEAR::isError($res = $this->_getResponse())) {
  299. return $res;
  300. } else {
  301. return true;
  302. }
  303. } else {
  304. return PEAR::raiseError('Not currently in TRANSACTION state');
  305. }
  306. }
  307. /**
  308. * Retrieves the contents of the named script
  309. *
  310. * @access private
  311. * @param string $scriptname Name of the script to retrieve
  312. * @return mixed The script if successful, PEAR_Error otherwise
  313. */
  314. function _cmdGetScript($scriptname)
  315. {
  316. if (NET_SIEVE_STATE_TRANSACTION === $this->_state) {
  317. $this->_sendCmd(sprintf('GETSCRIPT "%s"', $scriptname));
  318. if (PEAR::isError($res = $this->_getResponse())) {
  319. return $res;
  320. } else {
  321. return preg_replace('/{[0-9]+}\r\n/', '', $res);
  322. }
  323. } else {
  324. return PEAR::raiseError('Not currently in TRANSACTION state');
  325. }
  326. }
  327. /**
  328. * Sets the ACTIVE script, ie the one that gets run on new mail
  329. * by the server
  330. *
  331. * @access private
  332. * @param string $scriptname The name of the script to mark as active
  333. * @return mixed True on success, PEAR_Error otherwise
  334. */
  335. function _cmdSetActive($scriptname)
  336. {
  337. if (NET_SIEVE_STATE_TRANSACTION === $this->_state) {
  338. $this->_sendCmd(sprintf('SETACTIVE "%s"', $scriptname));
  339. if (PEAR::isError($res = $this->_getResponse())) {
  340. return $res;
  341. } else {
  342. $this->_activeScript = $scriptname;
  343. return true;
  344. }
  345. } else {
  346. return PEAR::raiseError('Not currently in TRANSACTION state');
  347. }
  348. }
  349. /**
  350. * Sends the LISTSCRIPTS command
  351. *
  352. * @access private
  353. * @return mixed Two item array of scripts, and active script on success,
  354. * PEAR_Error otherwise.
  355. */
  356. function _cmdListScripts()
  357. {
  358. if (NET_SIEVE_STATE_TRANSACTION === $this->_state) {
  359. $scripts = array();
  360. $activescript = null;
  361. $this->_sendCmd('LISTSCRIPTS');
  362. if (PEAR::isError($res = $this->_getResponse())) {
  363. return $res;
  364. } else {
  365. $res = explode("\r\n", $res);
  366. foreach ($res as $value) {
  367. if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) {
  368. $scripts[] = $matches[1];
  369. if (!empty($matches[2])) {
  370. $activescript = $matches[1];
  371. }
  372. }
  373. }
  374. return array($scripts, $activescript);
  375. }
  376. } else {
  377. return PEAR::raiseError('Not currently in TRANSACTION state');
  378. }
  379. }
  380. /**
  381. * Sends the PUTSCRIPT command to add a script to
  382. * the server.
  383. *
  384. * @access private
  385. * @param string $scriptname Name of the new script
  386. * @param string $scriptdata The new script
  387. * @return mixed True on success, PEAR_Error otherwise
  388. */
  389. function _cmdPutScript($scriptname, $scriptdata)
  390. {
  391. if (NET_SIEVE_STATE_TRANSACTION === $this->_state) {
  392. $this->_sendCmd(sprintf('PUTSCRIPT "%s" {%d+}', $scriptname, strlen($scriptdata)));
  393. $this->_sendCmd($scriptdata);
  394. if (!PEAR::isError($res = $this->_getResponse())) {
  395. return true;
  396. } else {
  397. return $res;
  398. }
  399. } else {
  400. return PEAR::raiseError('Not currently in TRANSACTION state');
  401. }
  402. }
  403. /**
  404. * Sends the LOGOUT command and terminates the connection
  405. *
  406. * @access private
  407. * @return mixed True on success, PEAR_Error otherwise
  408. */
  409. function _cmdLogout()
  410. {
  411. if (NET_SIEVE_STATE_DISCONNECTED !== $this->_state) {
  412. $this->_sendCmd('LOGOUT');
  413. if (!PEAR::isError($res = $this->_getResponse())) {
  414. $this->_sock->disconnect();
  415. $this->_state = NET_SIEVE_STATE_DISCONNECTED;
  416. return true;
  417. } else {
  418. return $res;
  419. }
  420. } else {
  421. return PEAR::raiseError('Not currently connected');
  422. }
  423. }
  424. /**
  425. * Sends the CAPABILITY command
  426. *
  427. * @access private
  428. * @return mixed True on success, PEAR_Error otherwise
  429. */
  430. function _cmdCapability()
  431. {
  432. if (NET_SIEVE_STATE_TRANSACTION === $this->_state) {
  433. $this->_sendCmd('CAPABILITY');
  434. if (!PEAR::isError($res = $this->_getResponse())) {
  435. $this->_parseCapability($res);
  436. return true;
  437. } else {
  438. return $res;
  439. }
  440. } else {
  441. return PEAR::raiseError('Not currently in TRANSACTION state');
  442. }
  443. }
  444. /**
  445. * Parses the response from the capability command. Stores
  446. * the result in $this->_capability
  447. *
  448. * @access private
  449. */
  450. function _parseCapability($data)
  451. {
  452. $data = preg_split('/\r?\n/', $data, -1, PREG_SPLIT_NO_EMPTY);
  453. for ($i = 0; $i < count($data); $i++) {
  454. if (preg_match('/^"([a-z]+)" ("(.*)")?$/i', $data[$i], $matches)) {
  455. switch (strtolower($matches[1])) {
  456. case 'implementation':
  457. $this->_capability['implementation'] = $matches[3];
  458. break;
  459. case 'sasl':
  460. $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]);
  461. break;
  462. case 'sieve':
  463. $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]);
  464. break;
  465. case 'starttls':
  466. $this->_capability['starttls'] = true;
  467. }
  468. }
  469. }
  470. }
  471. /**
  472. * Sends a command to the server
  473. *
  474. * @access private
  475. * @param string $cmd The command to send
  476. */
  477. function _sendCmd($cmd)
  478. {
  479. $this->_sock->writeLine($cmd);
  480. }
  481. /**
  482. * Retrieves a response from the server and, to a certain degree,
  483. * parses it.
  484. *
  485. * @access private
  486. * @return mixed Reponse string if an OK response, PEAR_Error if a NO response
  487. */
  488. function _getResponse()
  489. {
  490. $response = '';
  491. while (true) {
  492. $line = $this->_sock->readLine();
  493. if ('ok' == strtolower(substr($line, 0, 2))) {
  494. return rtrim($response);
  495. } elseif ('no' == strtolower(substr($line, 0, 2))) {
  496. // Check for string literal error message
  497. if (preg_match('/^no {([0-9]+)\+?}/i', $line, $matches)) {
  498. $line .= str_replace("\r\n", ' ', $this->_sock->read($matches[1]));
  499. }
  500. return PEAR::raiseError(trim($response . substr($line, 2)));
  501. } elseif ('bye' == strtolower(substr($line, 0, 3))) {
  502. if (preg_match('/^bye \((referral) "([^"]+)/i', $line, $matches)) {
  503. $line = $matches[1] . " " . $matches[2];
  504. }
  505. return PEAR::raiseError(trim($response . $line));
  506. }
  507. $response .= $line . "\r\n";
  508. }
  509. }
  510. }