PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://github.com/blekkzor/pinetd2
PHP | 331 lines | 295 code | 18 blank | 18 comment | 53 complexity | 010c491538c43ec370cc7162d2e370bb MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. namespace Daemon\PMaild;
  3. use pinetd\Logger;
  4. use pinetd\SQL;
  5. class MTA_Child extends \pinetd\ProcessChild {
  6. protected $sql;
  7. protected $localConfig;
  8. protected $mail = null;
  9. protected $ch;
  10. public function mainLoop($IPC) {
  11. $this->IPC = $IPC;
  12. $this->IPC->setParent($this);
  13. $this->localConfig = $this->IPC->getLocalConfig();
  14. $this->sql = SQL::Factory($this->localConfig['Storage']);
  15. $this->processBaseName = get_class($this);
  16. $this->setProcessStatus();
  17. while(1) {
  18. $this->IPC->selectSockets(0);
  19. // get a mail for me
  20. $req = 'SELECT `mlid`, `to` FROM `mailqueue` WHERE (`next_attempt` < NOW() OR `next_attempt` IS NULL) AND `pid` IS NULL ORDER BY `next_attempt` IS NULL, `next_attempt` LIMIT 10';
  21. $res = $this->sql->query($req);
  22. if (!$res) break;
  23. if ($res->num_rows < 1) break;
  24. while($row = $res->fetch_assoc()) {
  25. $req = 'UPDATE `mailqueue` SET `last_attempt` = NOW(), `pid` = \''.$this->sql->escape_string(getmypid()).'\' WHERE `mlid` = \''.$this->sql->escape_string($row['mlid']).'\' AND `to` = \''.$this->sql->escape_string($row['to']).'\' AND `pid` IS NULL';
  26. if (!$this->sql->query($req)) break;
  27. if ($this->sql->affected_rows < 1) continue; // missed it
  28. $DAO_mailqueue = $this->sql->DAO('mailqueue', array('mlid', 'to'));
  29. $mail = $DAO_mailqueue->loadByField($row);
  30. if (!$mail) continue; // ?!
  31. $mail = $mail[0];
  32. $this->track($mail, 'PROCESSING', '200 Mail being processed');
  33. $this->setProcessStatus($mail->to);
  34. $this->mail = $mail;
  35. // ok, we got our very own mlid
  36. try {
  37. $this->processEmail($mail);
  38. $this->mail = null;
  39. } catch(\Exception $e) {
  40. Logger::log(Logger::LOG_WARN, (string)$e);
  41. $this->mail = null;
  42. break;
  43. }
  44. }
  45. }
  46. }
  47. protected function track($mail, $status, $status_message, $host = 'none') {
  48. if (!$mail->tracker) return;
  49. if (!$this->ch) {
  50. $this->ch = curl_init();
  51. curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
  52. curl_setopt($this->ch, CURLOPT_HTTPHEADER, array('X-Ipc: MAIL_TRACK'));
  53. }
  54. curl_setopt($this->ch, CURLOPT_URL, $mail->tracker);
  55. $query = array(
  56. 'mlid' => $mail->mlid,
  57. 'status' => $status,
  58. 'status_host' => $host,
  59. 'status_message' => $status_message,
  60. );
  61. curl_setopt($this->ch, CURLOPT_POSTFIELDS, http_build_query($query, '', '&'));
  62. curl_exec($this->ch); // send query
  63. }
  64. public function shutdown() {
  65. // TODO: handle more CLEAN exit!
  66. $this->mail->pid = null;
  67. $this->mail->commit();
  68. exit;
  69. }
  70. protected function mailPath($uniq) {
  71. $path = $this->localConfig['Mails']['Path'].'/mailqueue';
  72. if ($path[0] != '/') $path = PINETD_ROOT . '/' . $path; // make it absolute
  73. $path.='/'.$uniq;
  74. return $path;
  75. }
  76. protected function processEmail($mail) {
  77. // we got from, we got to, analyze to
  78. $file = $this->mailPath($mail->mlid);
  79. $target = imap_rfc822_parse_adrlist($mail->to, '');
  80. $host = $target[0]->host;
  81. $this->track($mail, 'DNS_RESOLVE', '200 Resolving DNS for host '.$host);
  82. $mx = dns_get_record($host, DNS_MX);
  83. $count = 0;
  84. $mxlist = array();
  85. if ($mx) {
  86. foreach($mx as $mxhost) {
  87. if ($mxhost['type'] != 'MX') continue;
  88. $mxlist[$mxhost['target']] = $mxhost['pri'];
  89. }
  90. }
  91. $this->error = array();
  92. if (!$mxlist) { // No mx, RFC 2821 says to use the host itself with pri=0
  93. $mxlist[$host] = 0;
  94. }
  95. asort($mxlist);
  96. $status = 200;
  97. foreach($mxlist as $mx=>$pri) {
  98. try {
  99. if($this->processEmailMX($mx, $mail)) {
  100. // success -> cleanup a bit
  101. $mail->delete();
  102. $DAO_mailqueue = $this->sql->DAO('mailqueue', array('mlid', 'to'));
  103. $res = $DAO_mailqueue->loadByField(array('mlid' => $mail->mlid));
  104. if (!$res) @unlink($file); // cleanup
  105. return true;
  106. }
  107. } catch(\Exception $e) {
  108. Logger::log(Logger::LOG_INFO, 'MX '.$mx.' refused mail: '.$e->getMessage());
  109. $this->track($mail, 'ERROR', $e->getMessage(), $mx);
  110. $this->error[$mx] = $e;
  111. $status = (string)$e->getCode();
  112. if ($status[0] =='5') break; // fatal error
  113. }
  114. }
  115. if (($mail->attempt_count > $this->localConfig['MTA']['MaxAttempt']) || ($status[0] == '5')) {
  116. // TODO: handle Errors-To header field
  117. if (!is_null($mail->from)) {
  118. $class = relativeclass($this, 'MTA\\Mail');
  119. $txn = new $class(null, $this->IPC);
  120. $txn->setHelo($this->IPC->getName());
  121. $txn->setFrom(''); // no return path for errors
  122. $txn->addTarget($mail->from);
  123. $p = $txn->sendMail();
  124. fputs($p['fd'], 'Message-ID: <'.$mail->mlid.'-'.time().'@'.$this->IPC->getName().">\r\n");
  125. fputs($p['fd'], 'Date: '.date(DATE_RFC2822)."\r\n");
  126. fputs($p['fd'], 'From: MAILER-DAEMON <postmaster@'.$this->IPC->getName().">\r\n");
  127. fputs($p['fd'], 'X-User-Agent: pMaild v2.0.0'."\r\n");
  128. fputs($p['fd'], 'To: '.$mail->from."\r\n");
  129. fputs($p['fd'], 'Subject: Failed attempt to send email'."\r\n");
  130. fputs($p['fd'], "\r\n"); // end of headers
  131. fputs($p['fd'], "Hello,\r\n\r\nHere is the mailer daemon at ".$this->IPC->getName().". I'm sorry\r\n");
  132. fputs($p['fd'], "I couldn't transmit your mail. This is a fatal error, so I gave up\r\n\r\n");
  133. fputs($p['fd'], "Here are some details about the error:\r\n");
  134. foreach($this->error as $mx=>$msg) {
  135. fputs($p['fd'], ' '.$mx.': '.$msg->getMessage()."\r\n\r\n");
  136. }
  137. fputs($p['fd'], 'While delivering to: '.$mail->to."\r\n\r\n");
  138. $in = fopen($file, 'r');
  139. if ($in) {
  140. fputs($p['fd'], "And here are the headers of your mail, for reference:\r\n\r\n");
  141. while(!feof($in)) {
  142. $lin = fgets($in);
  143. if (rtrim($lin) == '') break;
  144. fputs($p['fd'], $lin);
  145. }
  146. fclose($in);
  147. }
  148. $txn->finishMail($this->IPC);
  149. }
  150. @unlink($file);
  151. $mail->delete();
  152. return;
  153. // throw new \Exception('Mail '.$mail->mlid.' for '.$mail->to.' refused: '.$info['last_error']);
  154. }
  155. // TODO: handle non-fatal errors and mail a delivery status *warning* if from is not null
  156. if ($mail->attempt_count == floor($this->localConfig['MTA']['MaxAttempt']*0.33)) {
  157. // this is thirddelivery, may be good to warn user
  158. if (!is_null($mail->from)) {
  159. $class = relativeclass($this, 'MTA\\Mail');
  160. $txn = new $class(null, $this->IPC);
  161. $txn->setHelo($this->IPC->getName());
  162. $txn->setFrom(''); // no return path for errors
  163. $txn->addTarget($mail->from);
  164. $p = $txn->sendMail();
  165. fputs($p['fd'], 'Message-ID: <'.$mail->mlid.'-'.time().'@'.$this->IPC->getName().">\r\n");
  166. fputs($p['fd'], 'Date: '.date(DATE_RFC2822)."\r\n");
  167. fputs($p['fd'], 'From: MAILER-DAEMON <postmaster@'.$this->IPC->getName().">\r\n");
  168. fputs($p['fd'], 'X-User-Agent: pMaild v2.0.0'."\r\n");
  169. fputs($p['fd'], 'To: '.$mail->from."\r\n");
  170. fputs($p['fd'], 'Subject: Warning: mail still pending, last attempt to send email failed'."\r\n");
  171. fputs($p['fd'], "\r\n"); // end of headers
  172. fputs($p['fd'], "Hello,\r\n\r\nHere is the mailer daemon at ".$this->IPC->getName().". I'm sorry\r\n");
  173. fputs($p['fd'], "I couldn't transmit your mail yet. This is NOT a fatal error, and I still have to retry a few times\r\n\r\n");
  174. fputs($p['fd'], "Here are some details about the warning:\r\n");
  175. foreach($this->error as $mx=>$msg) {
  176. fputs($p['fd'], ' '.$mx.': '.$msg->getMessage()."\r\n\r\n");
  177. }
  178. fputs($p['fd'], 'While delivering to: '.$mail->to."\r\n\r\n");
  179. $in = fopen($file, 'r');
  180. if ($in) {
  181. fputs($p['fd'], "And here are the headers of your mail, for reference:\r\n\r\n");
  182. while(!feof($in)) {
  183. $lin = fgets($in);
  184. if (rtrim($lin) == '') break;
  185. fputs($p['fd'], $lin);
  186. }
  187. fclose($in);
  188. }
  189. $txn->finishMail($this->IPC);
  190. }
  191. }
  192. // compute average retry time
  193. $maxlifetime = $this->localConfig['MTA']['MailMaxLifetime'] * 3600; // initially exprimed in hours
  194. $maxattempt = $this->localConfig['MTA']['MaxAttempt'];
  195. $avgwait = $maxlifetime / $maxattempt; // average wait time
  196. $mailpos = (float)$mail->attempt_count / $maxattempt; // position in queue as float, 0-1
  197. if ($mailpos > 1) $mailpos = -0.1;
  198. if ($mailpos == 0) $mailpos = -0.5; // if this is the first attempt, retry immediatly to avoid delays due to greylisting
  199. $shouldwait = $avgwait * ($mailpos + 0.5); // time we should wait for next attempt
  200. // store failure into db
  201. $mail->attempt_count ++;
  202. $mail->pid = null;
  203. $mail->next_attempt = $this->sql->timeStamp(time() + 3600);
  204. $mail->last_error = (string)array_pop($this->error);
  205. $mail->commit();
  206. return false;
  207. }
  208. protected function readMxAnswer($sock, $expect = 2, $saferead = false) {
  209. $res = array();
  210. while(1) {
  211. $lin = fgets($sock);
  212. if ($lin === false) throw new \Exception('Could not read from peer!', 400);
  213. $lin = rtrim($lin);
  214. $code = substr($lin, 0, 3);
  215. if ($lin[0] != $expect) throw new \Exception($lin, $code);
  216. $res[] = substr($lin, 4);
  217. if ($lin[3] != '-') break;
  218. }
  219. if (!$saferead) $this->IPC->selectSockets(0);
  220. return $res;
  221. }
  222. protected function writeMx($sock, $msg) {
  223. fputs($sock, $msg."\r\n");
  224. }
  225. protected function processEmailMX($host, $mail) {
  226. $file = $this->mailPath($mail->mlid);
  227. if (!file_exists($file)) throw new \Exception('Mail queued but file not found: '.$mail->mlid, 500);
  228. $size = filesize($file);
  229. $ssl = false;
  230. $capa = array();
  231. $this->IPC->selectSockets(0);
  232. $this->track($mail, 'CONNECT', '200 Connecting to host', $host);
  233. $sock = fsockopen($host, 25, $errno, $errstr, 30);
  234. stream_set_timeout($sock, 120); // 120 secs timeout on read
  235. if (!$sock) throw new \Exception('Connection failed: ['.$errno.'] '.$errstr, 400); // not fatal (400)
  236. $this->IPC->selectSockets(0);
  237. $this->readMxAnswer($sock); // hello man
  238. try {
  239. $this->writeMx($sock, 'EHLO '.$this->IPC->getName());
  240. $ehlo = $this->readMxAnswer($sock);
  241. } catch(\Exception $e) {
  242. // no ehlo? try helo
  243. $this->writeMx($sock, 'RSET');
  244. $this->readMxAnswer($sock);
  245. $this->writeMx($sock, 'HELO '.$this->IPC->getName());
  246. $this->readMxAnswer($sock);
  247. $ehlo = array();
  248. }
  249. // first, check for STARTTLS (will override $ehlo if present)
  250. foreach($ehlo as $cap) {
  251. if (strtoupper($cap) == 'STARTTLS') {
  252. // try to start ssl
  253. try {
  254. $this->writeMx($sock, 'STARTTLS');
  255. $this->readMxAnswer($sock);
  256. } catch(\Exception $e) {
  257. continue; // ignore it
  258. }
  259. if (!stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
  260. throw new \Exception('Failed to enable TLS on stream', 400);
  261. }
  262. $ssl = true;
  263. // need to EHLO again
  264. $this->writeMx($sock, 'EHLO '.$this->IPC->getName());
  265. $ehlo = $this->readMxAnswer($sock);
  266. break;
  267. }
  268. }
  269. // once more!
  270. foreach($ehlo as $cap) {
  271. $cap = strtolower($cap);
  272. if (substr($cap, 0, 5) == 'size ') {
  273. $capa['size'] = true;
  274. $maxsize = substr($cap, 5);
  275. if (($size > $maxsize) && ($maxsize > 0)) {
  276. throw new \Exception('Mail is too big for remote system', 500);
  277. }
  278. }
  279. }
  280. if ($capa['size']) {
  281. $this->writeMx($sock, 'MAIL FROM:<'.$mail->from.'> SIZE='.$size);
  282. } else {
  283. $this->writeMx($sock, 'MAIL FROM:<'.$mail->from.'>');
  284. }
  285. $this->readMxAnswer($sock);
  286. $this->writeMx($sock, 'RCPT TO:<'.$mail->to.'>');
  287. $this->readMxAnswer($sock);
  288. $this->track($mail, 'TRANSMIT', '200 Transmitting email', $host);
  289. $this->writeMx($sock, 'DATA');
  290. $this->readMxAnswer($sock, 3);
  291. $fp = fopen($file, 'r');
  292. //
  293. $class = relativeclass($this, 'MTA\\Mail');
  294. $MTA_Mail = new $class(null, $this->IPC);
  295. fputs($sock, $MTA_Mail->header('Received', '(PMaild MTA '.getmypid().' on '.$this->IPC->getName().' processing mail to '.$host.($ssl?' with TLS enabled':'').'); '.date(DATE_RFC2822)));
  296. while(!feof($fp)) {
  297. $lin = fgets($fp);
  298. if ($lin[0] == '.') $lin = '.'.$lin;
  299. fputs($sock, $lin);
  300. }
  301. $this->writeMx($sock, '.');
  302. $final = $this->readMxAnswer($sock, 2, true);
  303. $this->track($mail, 'SUCCESS', $final[0], $host);
  304. $this->writeMx($sock, 'QUIT');
  305. try {
  306. $this->readMxAnswer($sock, 2, true);
  307. } catch(\Exception $e) {
  308. // even if you complain now, it's too late
  309. }
  310. fclose($sock);
  311. return true;
  312. }
  313. }