PageRenderTime 72ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/code/classes/Daemon/PMaild/IMAP4_Client.class.php

https://github.com/blekkzor/pinetd2
PHP | 1391 lines | 1215 code | 96 blank | 80 comment | 184 complexity | 93ab6a428a9e9e6d8eb4ef3f270db281 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. // http://www.isi.edu/in-notes/rfc2683.txt (IMAP4 Implementation Recommendations)
  3. // http://www.faqs.org/rfcs/rfc3501.html (IMAP4rev1)
  4. // http://www.faqs.org/rfcs/rfc2045.html (Multipurpose Internet Mail Extensions (MIME) Part One)
  5. namespace Daemon\PMaild;
  6. use pinetd\SQL;
  7. use pinetd\SQL\Expr;
  8. class Quoted {
  9. private $value;
  10. public function __construct($value) {
  11. $this->value = $value;
  12. }
  13. public function __toString() {
  14. if (is_null($this->value)) return 'NIL';
  15. return '"'.addcslashes($this->value, '"').'"';
  16. }
  17. }
  18. class ArrayList {
  19. private $value;
  20. public function __construct($value) {
  21. $this->value = $value;
  22. }
  23. public function getValue() {
  24. return $this->value;
  25. }
  26. }
  27. class IMAP4_Client extends \pinetd\TCP\Client {
  28. protected $login = null;
  29. protected $info = null;
  30. protected $loggedin = false;
  31. protected $sql;
  32. protected $localConfig;
  33. protected $queryId = null;
  34. protected $selectedFolder = null;
  35. protected $uidmap = array();
  36. protected $reverseMap = array();
  37. protected $uidmap_next = 0;
  38. protected $idle_mode = false;
  39. protected $idle_queue = array();
  40. protected $idle_event = NULL;
  41. protected $recent = array();
  42. function __construct($fd, $peer, $parent, $protocol) {
  43. parent::__construct($fd, $peer, $parent, $protocol);
  44. $this->setMsgEnd("\r\n");
  45. }
  46. function welcomeUser() { // nothing to do
  47. return true;
  48. }
  49. protected function parseFetchParam($param, $kw = NULL) {
  50. // support for macros
  51. switch($kw) {
  52. case 'fetch':
  53. switch(strtoupper($param)) {
  54. case 'ALL': $param = '(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)'; break;
  55. case 'FAST': $param = '(FLAGS INTERNALDATE RFC822.SIZE)'; break;
  56. case 'FULL': $param = '(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)'; break;
  57. }
  58. break;
  59. }
  60. $param = rtrim($param);
  61. $result = array();
  62. $string = null;
  63. $reference = &$result;
  64. $len = strlen($param);
  65. $level = 0;
  66. $in_string = false;
  67. $ref = array(0 => &$result);
  68. for($i=0; $i<$len;$i++) {
  69. $c = $param[$i];
  70. if ($c == '"') {
  71. if (!$in_string) {
  72. if (!is_null($string)) throw new \Exception('Parse error');
  73. $in_string = true;
  74. $string = '';
  75. continue;
  76. }
  77. $reference[] = $string;
  78. $in_string = false;
  79. $string = null;
  80. continue;
  81. }
  82. if ($in_string) {
  83. $string .= $c;
  84. continue;
  85. }
  86. if ($c == '(') {
  87. $level++;
  88. $array = array();
  89. $ref[$level] = &$array;
  90. $reference[] = &$array;
  91. $reference = &$array;
  92. unset($array);
  93. continue;
  94. }
  95. if ($c == '[') {
  96. $level++;
  97. if (is_null($string)) throw new Exception('parse error');
  98. $array = array();
  99. $ref[$level] = &$array;
  100. $reference[$string] = &$array;
  101. $reference = &$array;
  102. unset($array);
  103. $string = null;
  104. continue;
  105. }
  106. if (($c == ')') || ($c == ']')) {
  107. $level--;
  108. if (!is_null($string)) $reference[] = $string;
  109. $string = null;
  110. $reference = &$ref[$level];
  111. continue;
  112. }
  113. if ($c == ' ') {
  114. if (is_null($string)) continue;
  115. $reference[] = $string;
  116. $string = null;
  117. continue;
  118. }
  119. if ($c == '{') { // string litteral (pending data, see RFC 3501 page 15)
  120. if (!is_null($string)) throw new Exception('parse error');
  121. $string = '';
  122. continue;
  123. }
  124. if ($c == '}') {
  125. if (is_null($string)) throw new Exception('parse error');
  126. if (!is_numeric($string)) throw new Exception('parse error');
  127. if ($i != ($len - 1)) throw new Exception('parse error'); // not at end of string
  128. $len = (int)$string;
  129. parent::sendMsg('+ Please continue'); // avoid tag
  130. $reference[] = $this->readTmpFd($len);
  131. $param = rtrim($this->readLine());
  132. $len = strlen($param);
  133. $i = -1; // will become 0 at next loop
  134. continue;
  135. }
  136. $string .= $c;
  137. }
  138. if (!is_null($string)) $result[] = $string;
  139. if (is_array($result[0])) $result = $result[0];
  140. unset($result['parent']);
  141. return $result;
  142. }
  143. function imapParam($str, $label=null) {
  144. if (is_null($str)) return 'NIL';
  145. if (is_array($str)) {
  146. $res = '';
  147. foreach($str as $lbl => $var) {
  148. $cur = $this->imapParam($var, $lbl);
  149. $res.=($res == ''?'':' ').$cur;
  150. }
  151. if (is_string($label)) {
  152. if ($res == '""')
  153. $res = '';
  154. return $label.'['.$res.']';
  155. }
  156. return '('.$res.')';
  157. }
  158. if ((is_object($str)) && ($str instanceof Quoted)) {
  159. return (string)$str;
  160. }
  161. if ((is_object($str)) && ($str instanceof ArrayList)) {
  162. $res = '';
  163. $val = $str->getValue();
  164. foreach($val as $var) {
  165. $res .= $this->imapParam($var);
  166. }
  167. if (is_string($label)) {
  168. return $label.'['.$res.']';
  169. }
  170. return $res;
  171. }
  172. if ($str === '') return '""';
  173. if (strpos($str, "\n") !== false) {
  174. return '{'.strlen($str).'}'."\r\n".$str; // TODO: is this linebreak ok?
  175. }
  176. $add = addcslashes($str, '"\'');
  177. if (($add == $str) && ($str != 'NIL') && (strpos($str, ' ') === false)) return $str;
  178. return '"'.$add.'"';
  179. }
  180. function sendBanner() {
  181. $this->sendMsg('OK '.$this->IPC->getName().' IMAP4rev1 2001.305/pMaild on '.date(DATE_RFC2822));
  182. $this->localConfig = $this->IPC->getLocalConfig();
  183. return true;
  184. }
  185. protected function parseLine($lin) {
  186. $lin = rtrim($lin); // strip potential \r and \n
  187. if ($this->idle_mode !== false) {
  188. if (strtolower($lin) != 'done') return;
  189. $this->sendMsg('OK IDLE terminated', $this->idle_mode);
  190. $this->idle_mode = false;
  191. return;
  192. }
  193. $match = array();
  194. $res = preg_match_all('/([^" ]+)|("(([^\\\\"]|(\\\\")|(\\\\\\\\))*)")/', $lin, $match);
  195. $argv = array();
  196. foreach($match[0] as $idx=>$arg) {
  197. if (($arg[0] == '"') && (substr($arg, -1) == '"')) {
  198. $argv[] = preg_replace('/\\\\(.)/', '\\1', $match[3][$idx]);
  199. continue;
  200. }
  201. $argv[] = $arg;
  202. }
  203. $this->queryId = array_shift($argv);
  204. $cmd = '_cmd_'.strtolower($argv[0]);
  205. if (!method_exists($this, $cmd)) $cmd = '_cmd_default';
  206. $res = $this->$cmd($argv, $lin);
  207. $this->queryId = null;
  208. return $res;
  209. }
  210. public function sendMsg($msg, $id=null) {
  211. if (is_null($id)) $id = $this->queryId;
  212. if (is_null($id)) $id = '*';
  213. return parent::sendMsg($id.' '.$msg);
  214. }
  215. protected function updateUidMap() {
  216. // compute uidmap and uidnext
  217. $this->debug('Updating UID map');
  218. $this->uidmap = array();
  219. $this->reverseMap = array();
  220. $pos = $this->selectedFolder;
  221. $req = 'SELECT `mailid` FROM `z'.$this->info['domainid'].'_mails` WHERE `userid` = \''.$this->sql->escape_string($this->info['account']->id).'\' ';
  222. $req.= 'AND `folder`=\''.$this->sql->escape_string($pos).'\' ';
  223. $req.= 'ORDER BY `mailid` ASC';
  224. $res = $this->sql->query($req);
  225. $id = 1;
  226. $uidnext = 1;
  227. while($row = $res->fetch_assoc()) {
  228. $this->uidmap[$id] = $row['mailid'];
  229. $this->reverseMap[$row['mailid']] = $id++;
  230. $uidnext = $row['mailid'] + 1;
  231. }
  232. $this->uidmap_next = $id;
  233. return $uidnext;
  234. }
  235. protected function allocateQuickId($uid) {
  236. $id = $this->uidmap_next++;
  237. $this->uidmap[$id] = $uid;
  238. $this->reverseMap[$uid] = $id;
  239. return $id;
  240. }
  241. function shutdown() {
  242. $this->sendMsg('BYE IMAP4 server is shutting down, please try again later', '*');
  243. }
  244. protected function identify($pass) { // login in $this->login
  245. $class = relativeclass($this, 'MTA\\Auth');
  246. $auth = new $class($this->localConfig);
  247. $this->loggedin = $auth->login($this->login, $pass, 'imap4');
  248. if (!$this->loggedin) return false;
  249. $this->login = $auth->getLogin();
  250. $info = $auth->getInfo();
  251. $this->info = $info;
  252. // link to MySQL
  253. $this->sql = SQL::Factory($this->localConfig['Storage']);
  254. return true;
  255. }
  256. protected function mailPath($uniq) {
  257. $path = $this->localConfig['Mails']['Path'].'/domains';
  258. if ($path[0] != '/') $path = PINETD_ROOT . '/' . $path; // make it absolute
  259. $id = $this->info['domainid'];
  260. $id = str_pad($id, 10, '0', STR_PAD_LEFT);
  261. $path .= '/' . substr($id, -1) . '/' . substr($id, -2) . '/' . $id;
  262. $id = $this->info['account']->id;
  263. $id = str_pad($id, 4, '0', STR_PAD_LEFT);
  264. $path .= '/' . substr($id, -1) . '/' . substr($id, -2) . '/' . $id;
  265. $path.='/'.$uniq;
  266. return $path;
  267. }
  268. function _cmd_default($argv, $lin) {
  269. $this->sendMsg('BAD Unknown command');
  270. var_dump($argv, $lin);
  271. }
  272. function _cmd_noop() {
  273. $this->sendMsg('OK NOOP completed');
  274. }
  275. function _cmd_check() {
  276. // RFC 3501 6.4.1. CHECK Command.
  277. // check is equivalent to "NOOP" if not needed
  278. $this->sendMsg('OK CHECK completed');
  279. }
  280. function _cmd_capability() {
  281. $secure = true;
  282. if ($this->protocol == 'tcp') $secure=false;
  283. $this->sendMsg('CAPABILITY IMAP4REV1 '.($secure?'':'STARTTLS ').'X-NETSCAPE NAMESPACE MAILBOX-REFERRALS SCAN SORT THREAD=REFERENCES THREAD=ORDEREDSUBJECT MULTIAPPEND LOGIN-REFERRALS IDLE AUTH='.($secure?'LOGIN':'LOGINDISABLED'), '*');
  284. $this->sendMsg('OK CAPABILITY completed');
  285. }
  286. function _cmd_logout() {
  287. $this->sendMsg('BYE '.$this->IPC->getName().' IMAP4rev1 server says bye!', '*');
  288. $this->sendMsg('OK LOGOUT completed');
  289. $this->close();
  290. if ($this->loggedin) {
  291. // Extra: update mail_count and mail_quota
  292. try {
  293. $this->sql->query('UPDATE `z'.$this->info['domainid'].'_accounts` AS a SET `mail_count` = (SELECT COUNT(1) FROM `z'.$this->info['domainid'].'_mails` AS b WHERE a.`id` = b.`userid`) WHERE a.`id` = \''.$this->sql->escape_string($this->info['account']->id).'\'');
  294. $this->sql->query('UPDATE `z'.$this->info['domainid'].'_accounts` AS a SET `mail_quota` = (SELECT SUM(b.`size`) FROM `z'.$this->info['domainid'].'_mails` AS b WHERE a.`id` = b.`userid`) WHERE a.`id` = \''.$this->sql->escape_string($this->info['account']->id).'\'');
  295. } catch(Exception $e) {
  296. // ignore it
  297. }
  298. }
  299. }
  300. function _cmd_starttls() {
  301. if (!$this->IPC->hasTLS()) {
  302. $this->sendMsg('NO SSL not available');
  303. return;
  304. }
  305. if ($this->protocol != 'tcp') {
  306. $this->sendMsg('BAD STARTTLS only available in PLAIN mode. An encryption mode is already enabled');
  307. return;
  308. }
  309. $this->sendMsg('OK STARTTLS completed');
  310. // TODO: this call will lock, need a way to avoid from doing it without Fork
  311. if (!stream_socket_enable_crypto($this->fd, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) {
  312. $this->sendMsg('BYE TLS negociation failed!', '*');
  313. $this->close();
  314. }
  315. $this->debug('SSL mode enabled');
  316. $this->protocol = 'tls';
  317. }
  318. function _cmd_login($argv) {
  319. // X LOGIN login password
  320. if ($this->loggedin) return $this->sendMsg('BAD Already logged in');
  321. if ($this->protocol == 'tcp') return $this->sendMsg('BAD Need SSL before logging in');
  322. $this->login = $argv[1];
  323. $pass = $argv[2];
  324. if (preg_match('/^{([0-9]+)}$/', $pass, $match)) {
  325. $this->sendMsg('Password', '+');
  326. $pass = rtrim($this->readLine()); // will be sent with CR LF
  327. $this->debug('Pass: '.$pass);
  328. }
  329. if (!$this->identify($pass)) {
  330. $this->sendMsg('NO Login or password are invalid.');
  331. return;
  332. }
  333. $this->sendMsg('OK LOGIN completed');
  334. }
  335. function _cmd_authenticate($argv) {
  336. if ($this->loggedin) return $this->sendMsg('BAD Already logged in');
  337. if ($this->protocol == 'tcp') return $this->sendMsg('BAD Need SSL before logging in');
  338. if (strtoupper($argv[1]) != 'LOGIN') {
  339. $this->sendMsg('BAD Unsupported auth method');
  340. return;
  341. }
  342. parent::sendMsg('+ '.base64_encode('User Name')); // avoid tag
  343. $res = $this->readLine();
  344. if ($res == '*') return $this->sendMsg('BAD AUTHENTICATE cancelled');
  345. $this->login = base64_decode($res);
  346. $this->debug('Login: '.$this->login);
  347. parent::sendMsg('+ '.base64_encode('Password')); // avoid tag
  348. $res = $this->readLine();
  349. if ($res == '*') return $this->sendMsg('BAD AUTHENTICATE cancelled');
  350. $pass = base64_decode($res);
  351. $this->debug('Pass: '.$pass);
  352. if(!$this->identify($pass)) {
  353. $this->sendMsg('NO AUTHENTICATE failed; login or password are invalid');
  354. return;
  355. }
  356. $this->sendMsg('OK AUTHENTICATE succeed');
  357. }
  358. function _cmd_namespace() {
  359. // * NAMESPACE (("" "/")("#mhinbox" NIL)("#mh/" "/")) (("~" "/")) (("#shared/" "/")("#ftp/" "/")("#news." ".")("#public/" "/"))
  360. // TODO: find some documentation and adapt this function
  361. // Documentation for namespaces : RFC2342
  362. if (!$this->loggedin) return $this->sendMsg('BAD Login needed');
  363. $this->sendMsg('NAMESPACE (("" "/")) NIL NIL', '*');
  364. $this->sendMsg('OK NAMESPACE completed');
  365. }
  366. function _cmd_lsub($argv) {
  367. if (!$this->loggedin) return $this->sendMsg('BAD Login needed');
  368. $namespace = $argv[1];
  369. $param = $argv[2];
  370. if ($namespace == '') $namespace = '/';
  371. if ($namespace != '/') {
  372. $this->sendMsg('NO Unknown namespace');
  373. return;
  374. }
  375. // TODO: Find doc and fix that according to correct process
  376. $this->sendMsg('LSUB () "/" INBOX', '*');
  377. $DAO_folders = $this->sql->DAO('z'.$this->info['domainid'].'_folders', 'id');
  378. $list = $DAO_folders->loadByField(array('account'=>$this->info['account']->id, 'subscribed' => 1));
  379. // cache list
  380. $cache = array(
  381. 0 => array(
  382. 'id' => 0,
  383. 'name' => 'INBOX',
  384. 'parent' => null,
  385. ),
  386. );
  387. foreach($list as $info) {
  388. $info['name'] = mb_convert_encoding($info['name'], 'UTF7-IMAP', 'UTF-8'); // convert UTF-8 -> modified UTF-7
  389. $cache[$info['id']] = $info;
  390. }
  391. // list folders in imap server
  392. foreach($list as $info) {
  393. $info = $cache[$info['id']];
  394. $name = $info['name'];
  395. $parent = $info['parent'];
  396. while(!is_null($parent)) {
  397. $info = $cache[$parent];
  398. $name = $info['name'].'/'.$name;
  399. $parent = $info['parent'];
  400. }
  401. $flags = '';
  402. if ($info['flags'] != '')
  403. foreach(explode(',', $info['flags']) as $f) $flags.=($flags==''?'':',').'\\'.ucfirst($f);
  404. $this->sendMsg('LSUB ('.$flags.') "/" '.$this->maybeQuote($name), '*');
  405. }
  406. $this->sendMsg('OK LSUB completed');
  407. }
  408. function imapWildcard($pattern, $string) {
  409. $pattern = preg_quote($pattern, '#');
  410. $pattern = str_replace('\\*', '.*', $pattern);
  411. $pattern = str_replace('%', '[^/]*', $pattern);
  412. return preg_match('#^'.$pattern.'$#', $string);
  413. }
  414. function _cmd_list($argv) {
  415. if (!$this->loggedin) return $this->sendMsg('BAD Login needed');
  416. $reference = $argv[1];
  417. $param = $argv[2];
  418. if ($param == '') {
  419. $this->sendMsg('LIST (\NoSelect) "/" ""', '*');
  420. $this->sendMsg('OK LIST completed');
  421. return;
  422. }
  423. if ($reference == '') $reference = '/';
  424. $name = $param;
  425. $DAO_folders = $this->sql->DAO('z'.$this->info['domainid'].'_folders', 'id');
  426. $parent = null;
  427. if ($reference != '/') {
  428. foreach(explode('/', $reference) as $ref) {
  429. if ($ref === '') continue;
  430. if ((is_null($parent)) && ($ref == 'INBOX')) {
  431. $parent = 0;
  432. continue;
  433. }
  434. $cond = array('account' => $this->info['account']->id, 'parent' => $parent, 'name' => $ref);
  435. $result = $DAO_folders->loadByField($cond);
  436. if (!$result) {
  437. $this->sendMsg('NO folder not found');
  438. return;
  439. }
  440. $parent = $result[0]->parent;
  441. }
  442. }
  443. $list = array();
  444. $fetch = array($parent);
  445. if (is_null($parent) && (fnmatch($param, 'INBOX'))) {
  446. $list[0] = array('name' => 'INBOX', 'children' => 0, 'parent' => null);
  447. $fetch[] = 0;
  448. }
  449. $cond = array('account' => $this->info['account']->id, 'parent' => $parent);
  450. // load whole tree, makes stuff easier - list should be recursive unless '%' is provided
  451. // start at parent
  452. $done = array();
  453. while($fetch) {
  454. $id = array_pop($fetch);
  455. if (isset($done[$id])) continue; // infinite loop
  456. $done[$id] = true;
  457. $cond['parent'] = $id;
  458. $result = $DAO_folders->loadByField($cond);
  459. foreach($result as $folder) {
  460. $folder = $folder->getProperties();
  461. if (is_null($folder['parent'])) $folder['parent'] = -1;
  462. if (isset($list[$folder['parent']])) {
  463. $folder['name'] = $list[$folder['parent']]['name'] . '/' . $folder['name'];
  464. $list[$folder['parent']]['children']++;
  465. }
  466. $fetch[] = $folder['id'];
  467. $folder['children'] = 0;
  468. $list[$folder['id']] = $folder;
  469. }
  470. }
  471. foreach($list as $res) {
  472. if (!$this->imapWildcard($param, $res['name'])) continue;
  473. $name = mb_convert_encoding($res['name'], 'UTF-8', 'UTF7-IMAP');
  474. $flags = array();
  475. if ($res['flags'] != '')
  476. foreach(explode(',', $res['flags']) as $f) $flags[]='\\'.ucfirst($f);
  477. if ($res['children'] == 0) {
  478. $flags[] = '\\HasNoChildren';
  479. } else {
  480. $flags[] = '\\HasChildren';
  481. }
  482. $this->sendMsg('LIST ('.implode(',',$flags).') "'.$reference.'" '.$this->maybeQuote($name), '*');
  483. }
  484. $this->sendMsg('OK LIST completed');
  485. }
  486. function maybeQuote($name) {
  487. if ($name === '') return '""';
  488. if ((strpos($name, ' ') === false) && (addslashes($name) == $name)) return $name;
  489. return '"'.addslashes($name).'"';
  490. }
  491. protected function lookupFolder($box) {
  492. $box = mb_convert_encoding($box, 'UTF-8', 'UTF7-IMAP,UTF-8'); // RFC says we should accept UTF-8
  493. $box = explode('/', $box);
  494. $pos = null;
  495. $DAO_folders = $this->sql->DAO('z'.$this->info['domainid'].'_folders', 'id');
  496. foreach($box as $name) {
  497. if ($name === '') continue;
  498. if (($name == 'INBOX') && (is_null($pos))) {
  499. $pos = 0;
  500. continue;
  501. }
  502. $result = $DAO_folders->loadByField(array('account' => $this->info['account']->id, 'name' => $name, 'parent' => $pos));
  503. if (!$result) {
  504. return NULL;
  505. }
  506. $pos = $result[0]->id;
  507. }
  508. $flags = array_flip(explode(',', $result[0]->flags));
  509. return array('id' => $pos, 'flags' => $flags);
  510. }
  511. function _cmd_select($argv) {
  512. if (!$this->loggedin) return $this->sendMsg('BAD Login needed');
  513. if (count($argv) != 2) {
  514. $this->sendMsg('BAD Please provide only one parameter to SELECT');
  515. return;
  516. }
  517. $box = $this->lookupFolder($argv[1]);
  518. if (is_null($box)) {
  519. $this->sendMsg('NO No such mailbox');
  520. return;
  521. }
  522. if (isset($box['flags']['noselect'])) return $this->sendMsg('NO This folder has \\Noselect flag');
  523. $this->selectedFolder = $box['id'];
  524. // TODO: find a way to do this without SQL code?
  525. $req = 'SELECT `flags`, COUNT(1) AS num FROM `z'.$this->info['domainid'].'_mails` WHERE `userid` = \''.$this->sql->escape_string($this->info['account']->id).'\' AND `folder` = \''.$this->sql->escape_string($this->selectedFolder).'\' GROUP BY `flags`';
  526. $res = $this->sql->query($req);
  527. $total = 0;
  528. $recent = 0;
  529. $unseen = 0;
  530. while($row = $res->fetch_assoc()) {
  531. $flags = array_flip(explode(',', $row['flags']));
  532. if (isset($flags['recent'])) $recent+=$row['num'];
  533. $total += $row['num'];
  534. }
  535. $uidnext = $this->updateUidMap();
  536. // search for unseen messages
  537. $req = 'SELECT `mailid` FROM `z'.$this->info['domainid'].'_mails` WHERE `userid` = \''.$this->sql->escape_string($this->info['account']->id).'\' ';
  538. $req.= 'AND `folder`=\''.$this->sql->escape_string($pos).'\' AND FIND_IN_SET(\'seen\',`flags`)=0 ';
  539. $req.= 'ORDER BY `mailid` ASC LIMIT 1';
  540. $res = $this->sql->query($req);
  541. if ($res) $res = $res->fetch_assoc();
  542. if ($res) {
  543. $unseen = $res['mailid'];
  544. $unseen = $this->reverseMap[$unseen];
  545. }
  546. if ($recent > 0) {
  547. // got recent mails, fetch their IDs
  548. $req = 'SELECT `mailid` FROM `z'.$this->info['domainid'].'_mails` WHERE `userid` = \''.$this->sql->escape_string($this->info['account']->id).'\' ';
  549. $req.= 'AND `folder`=\''.$this->sql->escape_string($pos).'\' AND FIND_IN_SET(\'recent\',`flags`)>0 ';
  550. $req.= 'ORDER BY `mailid` ASC';
  551. $res = $this->sql->query($req);
  552. while($row = $res->fetch_assoc()) {
  553. $this->recent[$row['mailid']] = $row['mailid'];
  554. $req = 'UPDATE `z'.$this->info['domainid'].'_mails` SET `flags` = REPLACE(`flags`,\'recent\',\'\')';
  555. $this->sql->query($req);
  556. }
  557. }
  558. // send response
  559. $this->sendMsg($total.' EXISTS', '*');
  560. $this->sendMsg($recent.' RECENT', '*');
  561. $this->sendMsg('OK [UIDVALIDITY '.$this->info['account']->id.'] UIDs valid', '*');
  562. $this->sendMsg('OK [UIDNEXT '.$uidnext.'] Predicted next UID', '*');
  563. $this->sendMsg('FLAGS (\Answered \Flagged \Deleted \Seen \Draft)', '*');
  564. $this->sendMsg('OK [PERMANENTFLAGS (\* \Answered \Flagged \Deleted \Draft \Seen)] Permanent flags', '*');
  565. if ($unseen) $this->sendMsg('OK [UNSEEN '.$unseen.'] Message '.$unseen.' is first recent', '*');
  566. if ($argv[0] == 'EXAMINE') {
  567. $this->sendMsg('OK [READ-ONLY] EXAMINE completed');
  568. return;
  569. }
  570. $this->sendMsg('OK [READ-WRITE] SELECT completed');
  571. $this->idleFolderChanged();
  572. }
  573. function _cmd_examine($argv) {
  574. $argv[0] = 'EXAMINE';
  575. return $this->_cmd_select($argv); // examine is the same, but read-only
  576. }
  577. function _cmd_create($argv) {
  578. $box = mb_convert_encoding($argv[1], 'UTF-8', 'UTF7-IMAP,UTF-8'); // RFC says we should accept UTF-8
  579. $box = explode('/', $box);
  580. $newbox = array_pop($box);
  581. $pos = null;
  582. $DAO_folders = $this->sql->DAO('z'.$this->info['domainid'].'_folders', 'id');
  583. foreach($box as $name) {
  584. if ($name === '') continue;
  585. if (($name == 'INBOX') && (is_null($pos))) {
  586. $pos = 0;
  587. continue;
  588. }
  589. $result = $DAO_folders->loadByField(array('account' => $this->info['account']->id, 'name' => $name, 'parent' => $pos));
  590. if (!$result) {
  591. $this->sendMsg('NO No such mailbox');
  592. return;
  593. }
  594. $pos = $result[0]->id;
  595. }
  596. if (is_null($pos) && ($newbox == 'INBOX')) {
  597. $this->sendMsg('NO Do not create INBOX, it already exists, damnit!');
  598. return;
  599. }
  600. $result = $DAO_folders->loadByField(array('account' => $this->info['account']->id, 'name' => $newbox, 'parent' => $pos));
  601. if ($result) {
  602. $result = $result[0];
  603. $flags = array_flip(explode(',', $result->flags));
  604. if (isset($flags['noselect'])) {
  605. $result->flags = ''; // clear flags
  606. $result->commit();
  607. $this->sendMsg('OK CREATE completed');
  608. return;
  609. }
  610. $this->sendMsg('NO Already exists');
  611. return;
  612. }
  613. $insert = array(
  614. 'account' => $this->info['account']->id,
  615. 'name' => $newbox,
  616. 'parent' => $pos,
  617. );
  618. if (!$DAO_folders->insertValues($insert)) {
  619. $this->sendMsg('NO Unknown error');
  620. return;
  621. }
  622. $this->sendMsg('OK CREATE completed');
  623. }
  624. function _cmd_delete($argv) {
  625. $box = mb_convert_encoding($argv[1], 'UTF-8', 'UTF7-IMAP,UTF-8'); // RFC says we should accept UTF-8
  626. $box = explode('/', $box);
  627. $pos = null;
  628. $DAO_folders = $this->sql->DAO('z'.$this->info['domainid'].'_folders', 'id');
  629. foreach($box as $name) {
  630. if ($name === '') continue;
  631. if (($name == 'INBOX') && (is_null($pos))) {
  632. $pos = 0;
  633. continue;
  634. }
  635. $result = $DAO_folders->loadByField(array('account' => $this->info['account']->id, 'name' => $name, 'parent' => $pos));
  636. if (!$result) {
  637. $this->sendMsg('NO No such mailbox');
  638. return;
  639. }
  640. $pos = $result[0]->id;
  641. }
  642. if ($pos === 0) {
  643. // RFC says deleting INBOX is an error (RFC3501, 6.3.4)
  644. $this->sendMsg('NO Do not delete INBOX, where will I be able to put your mails?!');
  645. return;
  646. }
  647. if (is_null($pos)) {
  648. $this->sendMsg('NO hey man! Do not delete root, would you?');
  649. return;
  650. }
  651. // delete box content
  652. $this->sql->query('DELETE mm, mmh, m FROM `z'.$this->info['domainid'].'_mime` AS mm, `z'.$this->info['domainid'].'_mime_header` AS mmh, `z'.$this->info['domainid'].'_mails` AS m WHERE m.`parent` = \''.$this->sql->escape_string($pos).'\' AND m.`account` = \''.$this->sql->escape_string($this->info['account']->id).'\' AND m.mailid = mm.mailid AND m.mailid = mmh.mailid');
  653. // check if box has childs
  654. $res = $DAO_folders->loadByField(array('account' => $this->info['account']->id, 'parent' => $pos));
  655. $result = $result[0]; // from the search loop
  656. if ($res) {
  657. $result->flags = 'noselect'; // put noselect flag
  658. $result->commit();
  659. $this->sendMsg('OK DELETE completed');
  660. return;
  661. }
  662. $result->delete();
  663. $this->sendMsg('OK DELETE completed');
  664. }
  665. function _cmd_close() {
  666. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  667. $DAO_mime = $this->sql->DAO('z'.$this->info['domainid'].'_mime', 'mimeid');
  668. $DAO_mime_header = $this->sql->DAO('z'.$this->info['domainid'].'_mime_header', 'headerid');
  669. $result = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'folder' => $this->selectedFolder, new Expr('FIND_IN_SET(\'deleted\',`flags`)>0')));
  670. foreach($result as $mail) {
  671. $DAO_mime->delete(array('userid' => $this->info['account']->id, 'mailid' => $mail->mailid));
  672. $DAO_mime_header->delete(array('userid' => $this->info['account']->id, 'mailid' => $mail->mailid));
  673. @unlink($this->mailPath($mail->uniqname));
  674. $mail->delete();
  675. }
  676. $this->selectedFolder = NULL;
  677. $this->idleFolderChanged();
  678. $this->sendMsg('OK CLOSE completed');
  679. }
  680. function _cmd_expunge() {
  681. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  682. $DAO_mime = $this->sql->DAO('z'.$this->info['domainid'].'_mime', 'mimeid');
  683. $DAO_mime_header = $this->sql->DAO('z'.$this->info['domainid'].'_mime_header', 'headerid');
  684. $result = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'folder' => $this->selectedFolder, new Expr('FIND_IN_SET(\'deleted\',`flags`)>0')));
  685. foreach($result as $mail) {
  686. $DAO_mime->delete(array('userid' => $this->info['account']->id, 'mailid' => $mail->mailid));
  687. $DAO_mime_header->delete(array('userid' => $this->info['account']->id, 'mailid' => $mail->mailid));
  688. @unlink($this->mailPath($mail->uniqname));
  689. $this->sendMsg($this->reverseMap[$mail->mailid].' EXPUNGE', '*');
  690. $this->IPC->broadcast('PMaild::Activity_'.$this->info['domainid'].'_'.$this->info['account']->id.'_'.$mail->folder, array($mail->mailid, 'EXPUNGE'));
  691. unset($this->reverseMap[$mail->mailid]);
  692. $mail->delete();
  693. }
  694. $this->sendMsg('OK EXPUNGE completed');
  695. }
  696. function fetchMailByUid(array $where, $param) {
  697. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  698. // TODO: implement headers fetch via mail class
  699. $result = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'folder' => $this->selectedFolder) + $where);
  700. if (!$result) return false;
  701. foreach($result as $mail) {
  702. $class = relativeclass($this, 'Mail');
  703. $omail = new $class($this->info, $mail, $this->mailPath($mail->uniqname), $this->sql);
  704. if (!$omail->valid()) {
  705. $omail->delete();
  706. continue;
  707. }
  708. $uid = $mail->mailid;
  709. if (!isset($this->reverseMap[$uid])) {
  710. // we do not know this mail, allocate an id quickly
  711. $id = $this->allocateQuickId($uid);
  712. } else {
  713. $id = $this->reverseMap[$uid];
  714. }
  715. $this->sendMsg($id.' FETCH '.$this->fetchParamByMail($omail, $param), '*');
  716. }
  717. return true;
  718. }
  719. function fetchMailById($id, $param) {
  720. // not in the current uidmap?
  721. if (!isset($this->uidmap[$id])) {
  722. return false;
  723. }
  724. $uid = $this->uidmap[$id];
  725. return $this->fetchMailByUid(array('mailid' => $uid), $param);
  726. }
  727. function fetchParamByMail($mail, $param) {
  728. $res = array();
  729. foreach($param as $id => $item) {
  730. if ((is_array($item)) && (is_int($id))) {
  731. $res[] = $this->fetchParamByMail($mail, $item);
  732. continue;
  733. }
  734. $item_param = null;
  735. if (!is_int($id)) {
  736. $item_param = $item;
  737. $item = $id;
  738. }
  739. switch(strtoupper($item)) {
  740. case 'UID':
  741. $res[] = 'UID';
  742. $res[] = $mail->getId();
  743. break;
  744. case 'ENVELOPE':
  745. $res[] = 'ENVELOPE';
  746. $res[] = $mail->getEnvelope();
  747. break;
  748. case 'BODY':
  749. if (is_null($item_param)) {
  750. // return bodystructure
  751. $res[] = 'BODY';
  752. $res[] = $mail->getStructure();
  753. break;
  754. }
  755. $this->storeFlags($mail, 'add', 'seen');
  756. case 'BODY.PEEK':
  757. $res_body = $mail->fetchBody($item_param);
  758. foreach($res_body as $t => $v) {
  759. if (is_string($t)) {
  760. $res[$t] = $v;
  761. continue;
  762. }
  763. $res[] = $v;
  764. }
  765. break;
  766. case 'RFC822.HEADER':
  767. $res[] = 'RFC822.HEADER';
  768. $res[] = $mail->fetchRfc822Headers();
  769. break;
  770. case 'BODYSTRUCTURE':
  771. $res[] = 'BODYSTRUCTURE';
  772. $res[] = $mail->getStructure(true); // get complete body structure
  773. break;
  774. case 'RFC822.SIZE': // TODO: determine if we should include headers in size
  775. $res[] = 'RFC822.SIZE';
  776. $res[] = $mail->size();
  777. break;
  778. case 'FLAGS':
  779. $f = array();
  780. if ($mail->flags != '') {
  781. $flags = explode(',', $mail->flags);
  782. foreach($flags as $flag) $f[] = '\\'.ucfirst($flag);
  783. }
  784. $res[] = 'FLAGS';
  785. $res[] = $f;
  786. break;
  787. case 'INTERNALDATE':
  788. $res[] = 'INTERNALDATE';
  789. $res[] = date(DATE_RFC2822, $mail->creationTime());
  790. break;
  791. default:
  792. var_dump($item, $item_param);
  793. $res[] = strtoupper($item);
  794. $res[] = NULL;
  795. break;
  796. }
  797. }
  798. return $this->imapParam($res);
  799. }
  800. function _cmd_subscribe($argv) {
  801. $box = $this->lookupFolder($argv[1]);
  802. if (is_null($box)) {
  803. $this->sendMsg('NO Folder not found');
  804. return;
  805. }
  806. if ($box['id'] == 0) {
  807. $this->sendMsg('NO INBOX cannot be subscribed');
  808. return;
  809. }
  810. // load bean
  811. $DAO_folders = $this->sql->DAO('z'.$this->info['domainid'].'_folders', 'id');
  812. $folder = $DAO_folders[$box['id']];
  813. $folder->subscribed = 1;
  814. $folder->commit();
  815. $this->sendMsg('OK SUBSCRIBE completed');
  816. }
  817. function _cmd_unsubscribe($argv) {
  818. $box = $this->lookupFolder($argv[1]);
  819. if (is_null($box)) {
  820. $this->sendMsg('NO Folder not found');
  821. return;
  822. }
  823. if ($box['id'] == 0) {
  824. $this->sendMsg('NO INBOX cannot be unsubscribed');
  825. return;
  826. }
  827. // load bean
  828. $DAO_folders = $this->sql->DAO('z'.$this->info['domainid'].'_folders', 'id');
  829. $folder = $DAO_folders[$box['id']];
  830. $folder->subscribed = 0;
  831. $folder->commit();
  832. $this->sendMsg('OK UNSUBSCRIBE completed');
  833. }
  834. /*
  835. A FETCH 1 (UID ENVELOPE BODY.PEEK[HEADER.FIELDS (Newsgroups Content-MD5 Content-Disposition Content-Language Content-Location Followup-To References)] INTERNALDATE RFC822.SIZE FLAGS)
  836. * 1 FETCH (UID 1170 ENVELOPE ("9 Aug 2005 18:25:47 -0000" "New graal.net Player World Submitted" ((NIL NIL "noreply" "graal.net")) ((NIL NIL "noreply" "graal.net")) ((NIL NIL "noreply" "graal.net")) ((NIL NIL "MagicalTux" "online.fr")) NIL NIL NIL "<20050809182547.3404.qmail@europa13.legende.net>") BODY[HEADER.FIELDS ("NEWSGROUPS" "CONTENT-MD5" "CONTENT-DISPOSITION" "CONTENT-LANGUAGE" "CONTENT-LOCATION" "FOLLOWUP-TO" "REFERENCES")] {2}
  837. INTERNALDATE " 9-Aug-2005 20:07:37 +0000" RFC822.SIZE 1171 FLAGS (\Seen))
  838. A OK FETCH completed
  839. A FETCH 1 (UID)
  840. * 1 FETCH (UID 1170)
  841. A FETCH 1 (ENVELOPE)
  842. * 1 FETCH (ENVELOPE ("9 Aug 2005 18:25:47 -0000" "New graal.net Player World Submitted" ((NIL NIL "noreply" "graal.net")) ((NIL NIL "noreply" "graal.net")) ((NIL NIL "noreply" "graal.net")) ((NIL NIL "MagicalTux" "online.fr")) NIL NIL NIL "<20050809182547.3404.qmail@europa13.legende.net>"))
  843. A OK FETCH completed
  844. A FETCH 1 BODY.PEEK[HEADER]
  845. * 1 FETCH (BODY[HEADER] {567}
  846. Return-Path: <noreply@graal.net>
  847. Delivered-To: online.fr-MagicalTux@online.fr
  848. Received: (qmail 29038 invoked from network); 9 Aug 2005 20:07:37 -0000
  849. Received: from europa13.legende.net (194.5.30.13)
  850. by mrelay5-1.free.fr with SMTP; 9 Aug 2005 20:07:37 -0000
  851. Received: (qmail 3405 invoked by uid 99); 9 Aug 2005 18:25:47 -0000
  852. Date: 9 Aug 2005 18:25:47 -0000
  853. Message-ID: <20050809182547.3404.qmail@europa13.legende.net>
  854. To: MagicalTux@online.fr
  855. From: <noreply@graal.net>
  856. Subject: New graal.net Player World Submitted
  857. Content-type: text/plain; charset=
  858. )
  859. A OK FETCH completed
  860. */
  861. // read it
  862. function _cmd_fetch($argv) {
  863. array_shift($argv); // FETCH
  864. $id = array_shift($argv); // might be "2:4"
  865. // parse param
  866. $param = implode(' ', $argv);
  867. // ok, let's parse param
  868. $param = $this->parseFetchParam($param, 'fetch');
  869. $last = null;
  870. while(strlen($id) > 0) {
  871. $pos = strpos($id, ':');
  872. $pos2 = strpos($id, ',');
  873. if ($pos === false) $pos = strlen($id);
  874. if ($pos2 === false) $pos2 = strlen($id);
  875. if ($pos < $pos2) {
  876. // got an interval. NB: 1:3:5 is impossible, must be 1:3,5 or something like that
  877. $start = substr($id, 0, $pos);
  878. $end = substr($id, $pos+1, $pos2 - $pos - 1);
  879. $id = substr($id, $pos2+1);
  880. if ($end == '*') {
  881. $i = $start;
  882. while($this->fetchMailById($i++, $param));
  883. continue;
  884. }
  885. for($i=$start; $i <= $end; $i++) {
  886. $this->fetchMailById($i, $param);
  887. }
  888. } else {
  889. $i = substr($id, 0, $pos2);
  890. $id = substr($id, $pos2+1);
  891. $this->fetchMailById($i, $param);
  892. }
  893. }
  894. $this->sendMsg('OK FETCH completed');
  895. }
  896. //A00008 UID FETCH 1:* (FLAGS RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT REFERENCES IN-REPLY-TO MESSAGE-ID MIME-VERSION CONTENT-TYPE X-MAILING-LIST X-LOOP LIST-ID LIST-POST MAILING-LIST ORIGINATOR X-LIST SENDER RETURN-PATH X-BEENTHERE)])
  897. function _cmd_uid($argv) {
  898. array_shift($argv); // UID
  899. $fetch = array_shift($argv); // FETCH
  900. // UID COPY, UID FETCH, UID STORE
  901. // UID SEARCH
  902. $func = '_cmd_uid_'.strtolower($fetch);
  903. if (method_exists($this, $func))
  904. return $this->$func($argv);
  905. $this->sendMsg('BAD Unsupported UID command ('.$fetch.')');
  906. return;
  907. }
  908. protected function _parseSearchCond(array $param) {
  909. $param = array_values($param); // just to be sure
  910. $where = array();
  911. for($i = 0; $i < count($param); $i++) {
  912. $t = strtoupper($param[$i]);
  913. switch($t) {
  914. case 'ALL': break;
  915. case 'UNSEEN': $where[] = 'FIND_IN_SET(\'seen\',`flags`)=0'; break;
  916. case 'ANSWERED': $where[] = 'FIND_IN_SET(\'answered\',`flags`)>0'; break;
  917. case 'DELETED': $where[] = 'FIND_IN_SET(\'deleted\',`flags`)>0'; break;
  918. case 'DRAFT': $where[] = 'FIND_IN_SET(\'draft\',`flags`)>0'; break;
  919. case 'FLAGGED': $where[] = 'FIND_IN_SET(\'flagged\',`flags`)>0'; break;
  920. case 'LARGER': $size = (int)$param[++$i]; $where[] = '`size` > '.$size; break;
  921. case 'NEW': $where[] = 'FIND_IN_SET(\'seen\',`flags`)=0'; $where[] = 'FIND_IN_SET(\'recent\',`flags`)>0'; break;
  922. case 'OLD': $where[] = 'FIND_IN_SET(\'recent\',`flags`)=0'; break;
  923. case 'RECENT': if (!$this->recent) { $where[] = '0'; break; } $where[] = '`mailid` IN ('.implode(',', $this->recent).')'; break;
  924. case 'SEEN': $where[] = 'FIND_IN_SET(\'seen\',`flags`)>0'; break;
  925. case 'SMALLER': $size = (int)$param[++$i]; $where[] = '`size` < '.$size; break;
  926. case 'UNANSWERED': $where[] = 'FIND_IN_SET(\'answered\',`flags`)=0'; break;
  927. case 'UNDELETED': $where[] = 'FIND_IN_SET(\'deleted\',`flags`)=0'; break;
  928. case 'UNDRAFT': $where[] = 'FIND_IN_SET(\'draft\',`flags`)=0'; break;
  929. case 'UNFLAGGED': $where[] = 'FIND_IN_SET(\'flagged\',`flags`)=0'; break;
  930. case 'UNSEEN': $where[] = 'FIND_IN_SET(\'seen\',`flags`)=0'; break;
  931. default: return false;
  932. }
  933. }
  934. return $where;
  935. }
  936. protected function _cmd_search($argv, $lin) {
  937. array_shift($argv); // "SEARCH"
  938. $param = implode(' ', $argv);
  939. $param = $this->parseFetchParam($param);
  940. if (strtoupper($param[0]) == 'CHARSET') {
  941. array_shift($param); // CHARSET
  942. $charset = strtoupper(array_shift($param)); // charset
  943. if (($charset != 'UTF-8') && ($charset != 'US-ASCII')) {
  944. $this->sendMsg('NO [BADCHARSET] UTF-8 US-ASCII');
  945. return;
  946. }
  947. }
  948. $where = $this->_parseSearchCond($param, $charset);
  949. if ($where === false) {
  950. var_dump($param);
  951. } else {
  952. $req = 'SELECT `mailid` FROM `z'.$this->info['domainid'].'_mails` WHERE `userid` = '.$this->sql->quote_escape($this->info['account']->id).' AND `folder` = '.$this->sql->quote_escape($this->selectedFolder);
  953. if ($where) $req.= ' AND '.implode(' AND ', $where);
  954. $req.= ' LIMIT 500';
  955. $res = $this->sql->query($req);
  956. while($row = $res->fetch_assoc()) {
  957. if ($lin == 'UID') {
  958. $final[] = $row['mailid'];
  959. } else {
  960. if (!isset($this->reverseMap[$uid])) {
  961. // we do not know this mail, allocate an id quickly
  962. $id = $this->allocateQuickId($row['mailid']);
  963. } else {
  964. $id = $this->reverseMap[$row['mailid']];
  965. }
  966. $final[] = $id;
  967. }
  968. }
  969. }
  970. if ($final) $this->sendMsg('SEARCH '.implode(' ', $final), '*');
  971. $this->sendMsg('OK SEARCH completed');
  972. }
  973. protected function _cmd_uid_search($argv) {
  974. array_unshift($argv, 'SEARCH');
  975. $this->_cmd_search($argv, 'UID');
  976. }
  977. protected function _cmd_uid_fetch($argv) {
  978. $id = array_shift($argv); // 1:*
  979. // parse param
  980. $param = implode(' ', $argv);
  981. // ok, let's parse param
  982. $param = $this->parseFetchParam($param, 'fetch');
  983. $param[] = 'UID';
  984. foreach($this->transformRange($id) as $where) {
  985. $this->fetchMailByUid($where, $param);
  986. }
  987. $this->sendMsg('OK FETCH completed');
  988. }
  989. protected function storeFlags($where, $mode, $flags) {
  990. if (is_object($where)) {
  991. $result = $where->getBean();
  992. } else {
  993. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  994. $result = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'folder' => $this->selectedFolder)+$where);
  995. }
  996. foreach($result as $mail) {
  997. if ($mail->flags == '') {
  998. $tmpfl = array();
  999. } else {
  1000. $tmpfl = explode(',', $mail->flags);
  1001. }
  1002. array_flip($tmpfl);
  1003. switch($mode) {
  1004. case 'set':
  1005. $tmpfl = array();
  1006. case 'add':
  1007. foreach($flags as $f)
  1008. $tmpfl[strtolower(substr($f, 1))] = strtolower(substr($f, 1));
  1009. break;
  1010. case 'sub':
  1011. foreach($flags as $f)
  1012. unset($tmpfl[strtolower(substr($f, 1))]);
  1013. break;
  1014. }
  1015. $mail->flags = implode(',', array_flip($tmpfl));
  1016. $mail->commit();
  1017. }
  1018. }
  1019. protected function transformRange($id) {
  1020. $res = array();
  1021. $last = null;
  1022. while(strlen($id) > 0) {
  1023. $pos = strpos($id, ':');
  1024. $pos2 = strpos($id, ',');
  1025. if ($pos === false) $pos = strlen($id);
  1026. if ($pos2 === false) $pos2 = strlen($id);
  1027. if ($pos < $pos2) {
  1028. // got an interval. NB: 1:3:5 is impossible, must be 1:3,5 or something like that
  1029. $start = substr($id, 0, $pos);
  1030. $end = substr($id, $pos+1, $pos2 - $pos - 1);
  1031. $id = substr($id, $pos2+1);
  1032. if ($end == '*') {
  1033. $where = array(new Expr('`mailid` >= '.$this->sql->quote_escape($start)));
  1034. } else {
  1035. $where = array();
  1036. $where[] = new Expr('`mailid` >= '.$this->sql->quote_escape($start));
  1037. $where[] = new Expr('`mailid` <= '.$this->sql->quote_escape($end));
  1038. }
  1039. $res[] = $where;
  1040. } else {
  1041. $i = substr($id, 0, $pos2);
  1042. $id = substr($id, $pos2+1);
  1043. $res[] = array('mailid' => $i);
  1044. }
  1045. }
  1046. return $res;
  1047. }
  1048. protected function _cmd_uid_store($argv) {
  1049. $id = array_shift($argv); // 1:*
  1050. $what = strtolower(array_shift($argv));
  1051. $mode = 'set';
  1052. $silent = false;
  1053. if ($what[0] == '+') {
  1054. $mode = 'add';
  1055. $what = substr($what, 1);
  1056. } else if ($what[0] == '-') {
  1057. $mode = 'sub';
  1058. $what = substr($what, 1);
  1059. }
  1060. if (substr($what, -7) == '.silent') {
  1061. $what = substr($what, 0, -7);
  1062. $silent = true;
  1063. }
  1064. if ($what != 'flags') {
  1065. $this->sendMsg('BAD Setting '.strtoupper($what).' not supported');
  1066. return;
  1067. }
  1068. $argv = implode(' ', $argv);
  1069. $flags = $this->parseFetchParam($argv);
  1070. foreach($this->transformRange($id) as $where) {
  1071. $this->storeFlags($where, $mode, $flags);
  1072. if (!$silent)
  1073. $this->fetchMailByUid($where, array('FLAGS'));
  1074. }
  1075. $this->sendMsg('OK STORE completed');
  1076. }
  1077. protected function _cmd_uid_copy($argv) {
  1078. // we will assume we never modify a file once received, and use link()
  1079. $id = array_shift($argv);
  1080. $box = $this->lookupFolder(array_shift($argv));
  1081. if (is_null($box)) {
  1082. $this->sendMsg('NO [TRYCREATE] No such mailbox');
  1083. return;
  1084. }
  1085. if (isset($box['flags']['noselect'])) return $this->sendMsg('NO This folder has \\Noselect flag');
  1086. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  1087. // invoke MailTarget
  1088. $class = relativeclass($this, 'MTA\\MailTarget');
  1089. $mailTarget = new $class('', '', $this->localConfig, $this->IPC);
  1090. foreach($this->transformRange($id) as $where) {
  1091. $result = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'folder' => $this->selectedFolder)+$where);
  1092. foreach($result as $mail) {
  1093. // copy this mail, but first generate an unique id
  1094. $new = $mailTarget->makeUniq('domains', $this->info['domainid'], $this->info['account']->id);
  1095. link($this->mailPath($mail->uniqname), $new);
  1096. $flags = array_flip(explode(',', $mail->flags));
  1097. $flags['recent'] = 'recent';
  1098. $flags = implode(',', array_flip($flags));
  1099. // insert mail
  1100. $DAO_mails->insertValues(array(
  1101. 'folder' => $box['id'],
  1102. 'userid' => $this->info['account']->id,
  1103. 'size' => $mail->size,
  1104. 'uniqname' => basename($new),
  1105. 'flags' => $flags,
  1106. ));
  1107. $newid = $this->sql->insert_id;
  1108. $this->IPC->broadcast('PMaild::Activity_'.$this->info['domainid'].'_'.$this->info['account']->id.'_'.$box['id'], array($newid, 'EXISTS'));
  1109. // copy headers
  1110. }
  1111. }
  1112. $this->sendMsg('OK COPY completed');
  1113. }
  1114. function _cmd_status($argv) {
  1115. // We got STATUS folder (...)
  1116. array_shift($argv); // "STATUS"
  1117. $box_name = array_shift($argv);
  1118. $box = $this->lookupFolder($box_name);
  1119. $opt = $this->parseFetchParam(implode(' ', $argv));
  1120. if (isset($box['flags']['noselect'])) return $this->sendMsg('NO This folder has \\Noselect flag');
  1121. // TODO: find a way to do this without SQL code?
  1122. $req = 'SELECT `flags`, COUNT(1) AS num, (MAX(`mailid`)+1) AS uidnext FROM `z'.$this->info['domainid'].'_mails` WHERE `userid` = \''.$this->sql->escape_string($this->info['account']->id).'\' AND `folder` = \''.$this->sql->escape_string($box['id']).'\' GROUP BY `flags`';
  1123. $res = $this->sql->query($req);
  1124. $total = 0;
  1125. $recent = 0;
  1126. $unseen = 0;
  1127. $uidnext = 0;
  1128. while($row = $res->fetch_assoc()) {
  1129. $flags = array_flip(explode(',', $row['flags']));
  1130. if (isset($flags['recent'])) $recent+=$row['num'];
  1131. $total += $row['num'];
  1132. if ($uidnext < $row['uidnext']) $uidnext = $row['uidnext'];
  1133. }
  1134. $res = array();
  1135. foreach($opt as $o) {
  1136. switch($o) {
  1137. case 'MESSAGES': // How many messsages
  1138. $res[] = 'MESSAGES';
  1139. $res[] = $total;
  1140. break;
  1141. case 'RECENT': // How many recent msg
  1142. $res[] = 'RECENT';
  1143. $res[] = $recent;
  1144. break;
  1145. case 'UIDNEXT': // next UID
  1146. $res[] = 'UIDNEXT';
  1147. $res[] = $uidnext;
  1148. case 'UIDVALIDITY': // uid validity
  1149. $res[] = 'UIDVALIDITY';
  1150. $res[] = $this->info['account']->id;
  1151. break;
  1152. case 'UNSEEN': // how many message do not have \seen
  1153. $res[] = 'UNSEEN';
  1154. $res[] = $unseen;
  1155. break;
  1156. }
  1157. }
  1158. $this->sendMsg('STATUS '.$box_name.' ('.implode(' ', $res).')', '*');
  1159. $this->sendMsg('OK STATUS completed');
  1160. }
  1161. protected function idleFolderChanged() {
  1162. $this->idle_queue = array();
  1163. if (!is_null($this->idle_event))
  1164. $this->IPC->unlistenBroadcast($this->idle_event, 'idle');
  1165. if (is_null($this->selectedFolder)) {
  1166. $this->idle_event = NULL;
  1167. return;
  1168. }
  1169. $this->idle_event = 'PMaild::Activity_'.$this->info['domainid'].'_'.$this->info['account']->id.'_'.$this->selectedFolder;
  1170. $this->IPC->listenBroadcast($this->idle_event, 'idle', array($this, 'receiveIdleEvent'));
  1171. }
  1172. public function receiveIdleEvent($data) {
  1173. $evt = NULL;
  1174. switch($data[1]) {
  1175. case 'EXISTS':
  1176. // new mail
  1177. $id = $this->allocateQuickId($data[0]);
  1178. $evt = $id.' EXISTS';
  1179. break;
  1180. case 'EXPUNGE':
  1181. if (!isset($this->reverseMap[$data[0]])) break;
  1182. $id = $this->reverseMap[$data[0]];
  1183. $evt = $id.' EXPUNGE';
  1184. unset($this->reverseMap[$data[0]]);
  1185. break;
  1186. }
  1187. if (is_null($evt)) return;
  1188. if ($this->idle_mode) {
  1189. $this->sendMsg($evt, '*'); // in idle mode, send notification right now
  1190. return;
  1191. }
  1192. $this->idle_queue[] = $evt; // queue for later, or for never
  1193. }
  1194. function _cmd_idle($argv) {
  1195. if (is_null($this->selectedFolder)) {
  1196. $this->sendMsg('NO no folder selected');
  1197. return;
  1198. }
  1199. $this->idle_mode = $this->queryId;
  1200. $this->sendMsg('idling', '+');
  1201. foreach($this->idle_queue as $line) $this->sendMsg($line, '*');
  1202. $this->idle_queue = array();
  1203. }
  1204. function _cmd_append($argv) {
  1205. // APPEND Folder Flagz Length
  1206. $length = array_pop($argv); // {xxx}
  1207. if (($length[0] != '{') || (substr($length, -1) != '}')) {
  1208. $this->sendMsg('NO Bad length');
  1209. return;
  1210. }
  1211. $length = (int)substr($length, 1, -1);
  1212. if (!$length) {
  1213. $this->sendMsg('NO Bad length');
  1214. return;
  1215. }
  1216. array_shift($argv); // "APPEND"
  1217. $box = $this->lookupFolder(array_shift($argv));
  1218. if (is_null($box)) {
  1219. $this->sendMsg('NO Mailbox not found');
  1220. return;
  1221. }
  1222. // we might get flags, and date? let's say we only have flags
  1223. $tmpflags = $this->parseFetchParam(implode(' ', $argv));
  1224. $flags = array();
  1225. foreach($tmpflags as $f) {
  1226. $f = strtolower(substr($f, 1));
  1227. $flags[$f] = $f;
  1228. }
  1229. // invoke MailTarget
  1230. $class = relativeclass($this, 'MTA\\MailTarget');
  1231. $mailTarget = new $class('', '', $this->localConfig, $this->IPC);
  1232. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  1233. // store this mail, but first generate an unique id
  1234. $new = $mailTarget->makeUniq('domains', $this->info['domainid'], $this->info['account']->id);
  1235. $fp = fopen($new, 'w+');
  1236. $this->sendMsg('Ready for literal data', '+');
  1237. $pos = 0;
  1238. while($pos < $length) {
  1239. $blen = $length - $pos;
  1240. if ($blen > 8192) $blen = 8192;
  1241. $buf = fread($this->fd, $blen);
  1242. fwrite($fp, $buf);
  1243. $pos += strlen($buf);
  1244. // failed upload ?
  1245. if (feof($this->fd)) {
  1246. fclose($fp);
  1247. @unlink($new);
  1248. return;
  1249. }
  1250. }
  1251. fgets($this->fd); // final ending empty line
  1252. fclose($fp);
  1253. $size = filesize($new);
  1254. // insert mail
  1255. $DAO_mails->insertValues($f = array(
  1256. 'folder' => $box['id'],
  1257. 'userid' => $this->info['account']->id,
  1258. 'size' => $size,
  1259. 'uniqname' => basename($new),
  1260. 'flags' => implode(',', $flags),
  1261. ));
  1262. $newid = $this->sql->insert_id;
  1263. $this->IPC->broadcast('PMaild::Activity_'.$this->info['domainid'].'_'.$this->info['account']->id.'_'.$box['id'], array($newid, 'EXISTS'));
  1264. $this->sendMsg('OK APPEND completed');
  1265. }
  1266. }