PageRenderTime 57ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://github.com/blekkzor/pinetd2
PHP | 435 lines | 393 code | 29 blank | 13 comment | 81 complexity | 8cf3dd4fff834d25db5c9136fb32fe63 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. namespace Daemon\PMaild;
  3. use pinetd\SQL;
  4. class POP3_Client extends \pinetd\TCP\Client {
  5. protected $login = null;
  6. protected $info = null;
  7. protected $loggedin = false;
  8. protected $localNum = array(0 => null);
  9. protected $toDelete = array(); // list of stuff that should be deleted on QUIT, and only on QUIT
  10. protected $sql;
  11. protected $localConfig;
  12. function __construct($fd, $peer, $parent, $protocol) {
  13. parent::__construct($fd, $peer, $parent, $protocol);
  14. $this->setMsgEnd("\r\n");
  15. }
  16. function welcomeUser() { // nothing to do
  17. return true;
  18. }
  19. function sendBanner() {
  20. $this->sendMsg('+OK '.$this->IPC->getName().' POP3 (pmaild v2.0.0; pinetd v'.PINETD_VERSION.')');
  21. $this->localConfig = $this->IPC->getLocalConfig();
  22. return true;
  23. }
  24. function shutdown() {
  25. $this->sendMsg('-ERR POP3 server is shutting down, please try again later');
  26. }
  27. protected function identify($pass) { // login in $this->login
  28. $class = relativeclass($this, 'MTA\Auth');
  29. $auth = new $class($this->localConfig);
  30. $this->loggedin = $auth->login($this->login, $pass, 'pop3');
  31. if (!$this->loggedin) return false;
  32. $this->login = $auth->getLogin();
  33. $info = $auth->getInfo();
  34. $this->info = $info;
  35. // link to MySQL
  36. $this->sql = SQL::Factory($this->localConfig['Storage']);
  37. return true;
  38. }
  39. // POP3 is rather annoying, we need to have our own list of ids specific to the current session ONLY
  40. protected function getLocalNum($id) {
  41. $tnum = array_search($id, $this->localNum);
  42. if ($tnum !== false) return $tnum; // found
  43. // allocate an id
  44. $this->localNum[] = $id;
  45. // return id
  46. return array_search($id, $this->localNum);
  47. }
  48. protected function resolveLocalNum($tnum) {
  49. return $this->localNum[$tnum];
  50. }
  51. protected function mailPath($uniq) {
  52. $path = $this->localConfig['Mails']['Path'].'/domains';
  53. if ($path[0] != '/') $path = PINETD_ROOT . '/' . $path; // make it absolute
  54. $id = $this->info['domainid'];
  55. $id = str_pad($id, 10, '0', STR_PAD_LEFT);
  56. $path .= '/' . substr($id, -1) . '/' . substr($id, -2) . '/' . $id;
  57. $id = $this->info['account']->id;
  58. $id = str_pad($id, 4, '0', STR_PAD_LEFT);
  59. $path .= '/' . substr($id, -1) . '/' . substr($id, -2) . '/' . $id;
  60. $path.='/'.$uniq;
  61. return $path;
  62. }
  63. function _cmd_default() {
  64. $this->sendMsg('-ERR Unknown command');
  65. }
  66. function _cmd_quit() {
  67. if ($this->loggedin) {
  68. // delete messages
  69. foreach($this->toDelete as $id => $mail) {
  70. $mailid = $mail->mailid;
  71. $file = $this->mailPath($mail->uniqname);
  72. unlink($file);
  73. clearstatcache();
  74. if (file_exists($file)) continue; // could not delete?
  75. $mail->delete();
  76. $this->sql->DAO('z'.$this->info['domainid'].'_mime', 'mimeid')->delete(array('userid'=>$this->info['account']->id, 'mailid'=>$mailid));
  77. $this->sql->DAO('z'.$this->info['domainid'].'_mime_header', 'headerid')->delete(array('userid'=>$this->info['account']->id, 'mailid'=>$mailid));
  78. }
  79. // Extra: update mail_count and mail_quota (for this user)
  80. try {
  81. $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).'\'');
  82. $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).'\'');
  83. } catch(Exception $e) {
  84. // ignore it
  85. }
  86. }
  87. $this->sendMsg('+OK '.$this->IPC->getName().' closing control connexion.');
  88. $this->close();
  89. }
  90. function _cmd_stls() {
  91. if (!$this->IPC->hasTLS()) {
  92. $this->sendMsg('-ERR SSL not available');
  93. return;
  94. }
  95. if ($this->protocol != 'tcp') {
  96. $this->sendMsg('-ERR STLS only available in PLAIN mode. An encryption mode is already enabled');
  97. return;
  98. }
  99. $this->sendMsg('+OK Begin TLS negotiation');
  100. // TODO: this call will lock, need a way to avoid from doing it without Fork
  101. if (!stream_socket_enable_crypto($this->fd, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) {
  102. $this->sendMsg('-ERR TLS negociation failed!');
  103. $this->close();
  104. }
  105. $this->protocol = 'tls';
  106. }
  107. function _cmd_auth($argv) {
  108. if ($this->loggedin) {
  109. $this->sendMsg('-ERR You do not need to AUTH two times');
  110. return;
  111. }
  112. if (($this->IPC->requireSsl()) && ($this->protocol == 'tcp')) return $this->sendMsg('-ERR Need SSL before logging in');
  113. if (!$argv[1]) {
  114. $this->sendMsg('+OK list of SASL extensions follows');
  115. $this->sendMsg('PLAIN');
  116. $this->sendMsg('.');
  117. return;
  118. }
  119. if ($argv[1] != 'PLAIN') {
  120. $this->sendMsg('-ERR Unknown AUTH method');
  121. return;
  122. }
  123. if (!$argv[2]) { // AUTH PLAIN <response>, or given on different line...
  124. $this->sendMsg('+ go ahead');
  125. $argv[2] = $this->readLine(); // TODO: not fork-compliant
  126. }
  127. $response = base64_decode($argv[2]);
  128. if ($response[0] != "\0") {
  129. $this->sendMsg('-ERR syntax error');
  130. return;
  131. }
  132. $response = explode("\0", $response);
  133. $this->login = $response[1];
  134. if ($this->identify($response[2])) {
  135. $this->sendMsg('+OK Authorization granted');
  136. } else {
  137. sleep(4);
  138. $this->sendMsg('-ERR Authorization denied');
  139. }
  140. }
  141. function _cmd_user($argv) {
  142. if ($this->loggedin) {
  143. $this->sendMsg('-ERR already logged in');
  144. return;
  145. }
  146. if (($this->IPC->requireSsl()) && ($this->protocol == 'tcp')) return $this->sendMsg('-ERR Need SSL before logging in');
  147. if (count($argv) < 2) {
  148. $this->sendMsg('-ERR Syntax: USER <login>');
  149. return;
  150. }
  151. $this->login = $argv[1];
  152. $this->sendMsg('+OK Provide PASSword now, please');
  153. }
  154. function _cmd_pass($argv) {
  155. if ($this->loggedin) {
  156. $this->sendMsg('-ERR Already logged in');
  157. return;
  158. }
  159. if (($this->IPC->requireSsl()) && ($this->protocol == 'tcp')) return $this->sendMsg('-ERR Need SSL before logging in');
  160. if (count($argv) < 2) {
  161. $this->sendMsg('-ERR Syntax: PASS <password>');
  162. return;
  163. }
  164. if ($this->identify($argv[1])) {
  165. $this->sendMsg('+OK Authorization granted');
  166. } else {
  167. sleep(4);
  168. $this->sendMsg('-ERR Authorization denied');
  169. }
  170. }
  171. function _cmd_noop() {
  172. if (!$this->loggedin) return $this->sendMsg('-ERR need to login first');
  173. $this->sendMsg('+OK'); // nothing :D
  174. }
  175. function _cmd_rset() {
  176. if (!$this->loggedin) return $this->sendMsg('-ERR need to login first');
  177. $this->toDelete = array();
  178. $this->sendMsg('+OK');
  179. }
  180. function _cmd_stat() {
  181. if (!$this->loggedin) return $this->sendMsg('-ERR need to login first');
  182. $this->sendMsg('+OK '.$this->info['account']->mail_count.' '.$this->info['account']->mail_quota);
  183. }
  184. function _cmd_list($argv) {
  185. if (!$this->loggedin) return $this->sendMsg('-ERR need to login first');
  186. $cond = array('userid' => $this->info['account']->id);
  187. $onlyone = false;
  188. if ($argv[1]) {
  189. $id = $argv[1];
  190. if (isset($this->toDelete[$id])) {
  191. $this->sendMsg('-ERR Message was deleted, you can still restore it using RSET');
  192. return;
  193. }
  194. $tid = $this->resolveLocalNum($id);
  195. if (is_null($tid)) {
  196. $this->sendMsg('-ERR Unknown message');
  197. return;
  198. }
  199. $cond['mailid'] = $tid;
  200. $onlyone = true;
  201. } else {
  202. $this->sendMsg('+OK'); // list...
  203. }
  204. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  205. $list = $DAO_mails->loadByField($cond);
  206. foreach($list as $mail) {
  207. $flags = array_flip(explode(',', $mail->flags));
  208. if (isset($flags['deleted'])) continue;
  209. $num = $this->getLocalNum($mail->mailid);
  210. if (isset($this->toDelete[$num])) continue;
  211. $file = $this->mailPath($mail->uniqname);
  212. if (!file_exists($file)) {
  213. $mail->delete();
  214. continue;
  215. }
  216. $s = filesize($file);
  217. if ($onlyone) {
  218. $this->sendMsg('+OK '.$num.' '.$s);
  219. return;
  220. }
  221. $this->sendMsg($num.' '.$s);
  222. }
  223. $this->sendMsg('.');
  224. }
  225. function _cmd_uidl($argv) {
  226. if (!$this->loggedin) return $this->sendMsg('-ERR need to login first');
  227. $cond = array('userid' => $this->info['account']->id);
  228. $onlyone = false;
  229. if ($argv[1]) {
  230. $id = $argv[1];
  231. if (isset($this->toDelete[$id])) {
  232. $this->sendMsg('-ERR Message was deleted, you can still restore it using RSET');
  233. return;
  234. }
  235. $tid = $this->resolveLocalNum($id);
  236. if (is_null($tid)) {
  237. $this->sendMsg('-ERR Unknown message');
  238. return;
  239. }
  240. $cond['mailid'] = $tid;
  241. $onlyone = true;
  242. } else {
  243. $this->sendMsg('+OK'); // list...
  244. }
  245. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  246. $list = $DAO_mails->loadByField($cond);
  247. foreach($list as $mail) {
  248. $flags = array_flip(explode(',', $mail->flags));
  249. if (isset($flags['deleted'])) continue;
  250. $num = $this->getLocalNum($mail->mailid);
  251. if (isset($this->toDelete[$num])) continue;
  252. $s = $mail->uniqname;
  253. if ($onlyone) {
  254. $this->sendMsg('+OK '.$num.' '.$s);
  255. return;
  256. }
  257. $this->sendMsg($num.' '.$s);
  258. }
  259. $this->sendMsg('.');
  260. }
  261. function _cmd_retr($argv) {
  262. $id = (int)$argv[1];
  263. if (isset($this->toDelete[$id])) {
  264. $this->sendMsg('-ERR Message was deleted');
  265. return;
  266. }
  267. $tid = $this->resolveLocalNum($id);
  268. if (is_null($tid)) {
  269. $this->sendMsg('-ERR Message not found');
  270. return;
  271. }
  272. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  273. $mail = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'mailid' => $tid));
  274. if (!$mail) {
  275. $this->sendMsg('-ERR Message not found');
  276. return;
  277. }
  278. $mail = $mail[0];
  279. $flags = array_flip(explode(',', $mail->flags));
  280. if (isset($flags['deleted'])) {
  281. $this->sendMsg('-ERR Message not found');
  282. return;
  283. }
  284. $file = $this->mailPath($mail->uniqname);
  285. $fd = fopen($file, 'r');
  286. if (!$fd) { // something is wrong with filesystem
  287. $this->sendMsg('+OK');
  288. $this->sendMsg('.');
  289. return;
  290. }
  291. $this->sendMsg('+OK');
  292. while(!feof($fd)) {
  293. $lin = fgets($fd);
  294. if ($lin[0] == '.') fputs($this->fd, '.');
  295. fputs($this->fd, $lin);
  296. }
  297. fclose($fd);
  298. $this->sendMsg('.');
  299. // mark the message as "read"
  300. $flags = array_flip(explode(',', $mail->flags));
  301. if (isset($flags['recent'])) {
  302. unset($flags['recent']);
  303. $mail->flags = implode(',', array_flip($flags));
  304. $mail->commit();
  305. }
  306. }
  307. function _cmd_top($argv) {
  308. // return headers plus n lines of mail
  309. $id = (int)$argv[1];
  310. $count = (int)$argv[2];
  311. if (isset($this->toDelete[$id])) {
  312. $this->sendMsg('-ERR Message was deleted');
  313. return;
  314. }
  315. $tid = $this->resolveLocalNum($id);
  316. if (is_null($tid)) {
  317. $this->sendMsg('-ERR Message not found');
  318. return;
  319. }
  320. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  321. $mail = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'mailid' => $tid));
  322. if (!$mail) {
  323. $this->sendMsg('-ERR Message not found');
  324. return;
  325. }
  326. $mail = $mail[0];
  327. $flags = array_flip(explode(',', $mail->flags));
  328. if (isset($flags['deleted'])) {
  329. $this->sendMsg('-ERR Message not found');
  330. return;
  331. }
  332. $file = $this->mailPath($mail->uniqname);
  333. $fd = fopen($file, 'r');
  334. if (!$fd) { // something is wrong with filesystem
  335. $this->sendMsg('+OK');
  336. $this->sendMsg('.');
  337. return;
  338. }
  339. $this->sendMsg('+OK');
  340. $h = true;
  341. while(!feof($fd)) {
  342. $lin = fgets($fd);
  343. if ($h) {
  344. if (rtrim($lin) === '') $h = false;
  345. } else {
  346. if($count--<=0) break;
  347. }
  348. if ($lin[0] == '.') fputs($this->fd, '.');
  349. fputs($this->fd, $lin);
  350. }
  351. fclose($fd);
  352. $this->sendMsg('.');
  353. // mark the message as "read"
  354. $flags = array_flip(explode(',', $mail->flags));
  355. if (isset($flags['recent'])) {
  356. unset($flags['recent']);
  357. $mail->flags = implode(',', array_flip($flags));
  358. $mail->commit();
  359. }
  360. }
  361. function _cmd_dele($argv) {
  362. $id = (int)$argv[1];
  363. if (isset($this->toDelete[$id])) {
  364. $this->sendMsg('-ERR Message was deleted');
  365. return;
  366. }
  367. $tid = $this->resolveLocalNum($id);
  368. if (is_null($tid)) {
  369. $this->sendMsg('-ERR Message not found');
  370. return;
  371. }
  372. $DAO_mails = $this->sql->DAO('z'.$this->info['domainid'].'_mails', 'mailid');
  373. $mail = $DAO_mails->loadByField(array('userid' => $this->info['account']->id, 'mailid' => $tid));
  374. if (!$mail) {
  375. $this->sendMsg('-ERR Message not found');
  376. return;
  377. }
  378. $mail = $mail[0];
  379. $flags = array_flip(explode(',', $mail->flags));
  380. if (isset($flags['deleted'])) {
  381. $this->sendMsg('-ERR Message not found');
  382. return;
  383. }
  384. $this->toDelete[$id] = $mail;
  385. $this->sendMsg('+OK Nessage marked for deletion');
  386. }
  387. function _cmd_capa($argv) {
  388. $capa = array(
  389. 'TOP',
  390. // 'RESP-CODES',
  391. 'PIPELINING',
  392. 'UIDL',
  393. 'IMPLEMENTATION pMaild 2.0',
  394. // 'AUTH-RESP-CODE',
  395. );
  396. if ($this->protocol != 'tcp') {
  397. $capa[] = 'USER';
  398. $capa[] = 'SASL PLAIN'; // SASL CRAM-MD5 DIGEST-MD5 PLAIN
  399. } else {
  400. $capa[] = 'STLS';
  401. }
  402. $this->sendMsg('+OK');
  403. foreach($capa as $cap) $this->sendMsg($cap);
  404. $this->sendMsg('.');
  405. }
  406. }