PageRenderTime 74ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/code/classes/Daemon/PMaild/MTA/MailTarget.class.php

https://github.com/blekkzor/pinetd2
PHP | 382 lines | 327 code | 34 blank | 21 comment | 65 complexity | 9069afae420a43e1b5d2e7be2449a704 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. namespace Daemon\PMaild\MTA;
  3. use pinetd\Logger;
  4. use pinetd\SQL;
  5. class MailTarget {
  6. protected $target;
  7. protected $from;
  8. protected $localConfig;
  9. protected $sql;
  10. protected $IPC;
  11. function __construct($target, $from, $localConfig, $IPC) {
  12. // transmit mail to target
  13. $this->target = $target;
  14. $this->from = $from;
  15. $this->localConfig = $localConfig;
  16. $this->sql = SQL::Factory($this->localConfig['Storage']);
  17. $this->IPC = $IPC;
  18. }
  19. public function makeUniq($path, $domain=null, $account=null) {
  20. $path = $this->localConfig['Mails']['Path'].'/'.$path;
  21. if ($path[0] != '/') $path = PINETD_ROOT . '/' . $path; // make it absolute
  22. if (!is_null($domain)) {
  23. $id = $domain;
  24. $id = str_pad($id, 10, '0', STR_PAD_LEFT);
  25. $path .= '/' . substr($id, -1) . '/' . substr($id, -2) . '/' . $id;
  26. }
  27. if (!is_null($account)) {
  28. $id = $account;
  29. $id = str_pad($id, 4, '0', STR_PAD_LEFT);
  30. $path .= '/' . substr($id, -1) . '/' . substr($id, -2) . '/' . $id;
  31. }
  32. if (!is_dir($path)) mkdir($path, 0755, true);
  33. $path .= '/'.microtime(true).'.'.gencode(5).getmypid().'.'.$this->localConfig['Name']['_'];
  34. return $path;
  35. }
  36. function runProtections(&$txn) {
  37. $domain = $this->target['domainbean'];
  38. if ($domain->antivirus) {
  39. $class = relativeclass($this, 'MailFilter\\AntiVirus');
  40. $antivirus = new $class($domain->antivirus, $domain, $this->localConfig);
  41. $res = $antivirus->process($txn);
  42. if (!is_null($res)) return $res;
  43. }
  44. if ($domain->antispam) {
  45. $class = relativeclass($this, 'MailFilter\\AntiSpam');
  46. $antispam = new $class($domain->antispam, $domain, $this->localConfig);
  47. $res = $antispam->process($txn);
  48. if (!is_null($res)) return $res;
  49. }
  50. }
  51. function processLocal(&$txn) {
  52. $res = $this->runProtections($txn);
  53. if (!is_null($res)) return $res;
  54. $domain = $this->target['domainbean'];
  55. $domain->last_recv = gmdate('Y-m-d H:i:s');
  56. $domain->commit();
  57. // invoke DAOs
  58. $DAO_accounts = $this->sql->DAO('z'.$this->target['domainid'].'_accounts', 'id');
  59. $DAO_mails = $this->sql->DAO('z'.$this->target['domainid'].'_mails', 'mailid');
  60. $DAO_filter = $this->sql->DAO('z'.$this->target['domainid'].'_filter', 'id');
  61. $DAO_filter_cond = $this->sql->DAO('z'.$this->target['domainid'].'_filter_cond', 'id');
  62. $DAO_filter_act = $this->sql->DAO('z'.$this->target['domainid'].'_filter_act', 'id');
  63. // need to store mail, index headers, etc
  64. $store = $this->makeUniq('domains', $this->target['domainid'], $this->target['target']);
  65. $out = fopen($store, 'w+');
  66. fputs($out, 'Return-Path: <'.$this->from.">\r\n");
  67. fputs($out, 'X-Original-To: <'.$this->target['mail_user'].'@'.$this->target['mail_domain'].">\r\n");
  68. fputs($out, 'Delivered-To: <'.$this->target['target_mail'].">\r\n");
  69. rewind($txn['fd']);
  70. stream_copy_to_stream($txn['fd'], $out);
  71. rewind($out);
  72. $headers = array();
  73. $last = '';
  74. while(!feof($out)) {
  75. $lin = rtrim(fgets($out));
  76. if ($lin === '') break; // end of headers
  77. $pos = false;
  78. if ($lin[0] != "\t")
  79. $pos = strpos($lin, ':');
  80. if ($pos !== false) {
  81. $last = &$headers[];
  82. $last['header'] = substr($lin, 0, $pos);
  83. $last = &$last['value'];
  84. $lin = substr($lin, $pos+1);
  85. }
  86. $last .= ' '.ltrim($lin);
  87. }
  88. foreach($headers as &$last) $last['value'] = ltrim($last['value']);
  89. unset($last);
  90. fclose($out);
  91. $size = filesize($store); // final stored size
  92. $quick_headers = array();
  93. foreach($headers as $h) $quick_headers[trim(strtolower($h['header']))] = trim($h['value']);
  94. // get root folder for this user
  95. $folder = 0; // root folder, for all accounts
  96. $flags = 'recent';
  97. // manage filters
  98. // $list = $DAO_filters->loadByField(array('userid' => $this->target['target']), array('order' => 'DESC'));
  99. foreach($DAO_filter->loadByField(array('userid' => $this->target['target'])) as $rule) {
  100. $match = true;
  101. foreach($DAO_filter_cond->loadByField(array('filterid' => $rule->id), array('priority' => 'DESC')) as $cond) {
  102. switch($cond->source) {
  103. case 'header':
  104. $arg = strtolower($cond->arg1);
  105. if (!isset($quick_headers[$arg])) {
  106. $match = false;
  107. break;
  108. }
  109. $value = $quick_headers[$arg];
  110. break;
  111. }
  112. if (!$match) break;
  113. switch($cond->type) {
  114. case 'exact':
  115. if ($value != $cond->arg2)
  116. $match = false;
  117. break;
  118. case 'contains':
  119. if (strpos($value, $cond->arg2) === false)
  120. $match = false;
  121. break;
  122. case 'preg':
  123. if (!preg_match($cond->arg2, $value))
  124. $match = false;
  125. break;
  126. }
  127. if (!$match) break;
  128. }
  129. if (!$match) continue;
  130. foreach($DAO_filter_act->loadByField(array('filterid' => $rule->id)) as $act) {
  131. switch($act->action) {
  132. case 'move':
  133. $folder = $act->arg1;
  134. break;
  135. // case 'drop':
  136. // $folder = -1;
  137. // break;
  138. case 'flags':
  139. $flags = $act->arg1;
  140. break;
  141. }
  142. }
  143. }
  144. // store mail
  145. $insert = array(
  146. 'folder' => $folder,
  147. 'userid' => $this->target['target'],
  148. 'size' => $size,
  149. 'uniqname' => basename($store),
  150. 'flags' => $flags,
  151. );
  152. $DAO_mails->insertValues($insert);
  153. $new = $DAO_mails->loadLast();
  154. $newid = $new->mailid;
  155. // at this point, the mail is successfully received ! Yatta!
  156. $this->IPC->broadcast('PMaild::Activity_'.$this->target['domainid'].'_'.$this->target['target'].'_'.$folder, array($newid, 'EXISTS'));
  157. // Extra: update mail_count and mail_quota
  158. try {
  159. $this->sql->query('UPDATE `z'.$this->target['domainid'].'_accounts` AS a SET `mail_count` = (SELECT COUNT(1) FROM `z'.$this->target['domainid'].'_mails` AS b WHERE a.`id` = b.`userid`) WHERE a.`id` = \''.$this->sql->escape_string($this->target['target']).'\'');
  160. $this->sql->query('UPDATE `z'.$this->target['domainid'].'_accounts` AS a SET `mail_quota` = (SELECT SUM(b.`size`) FROM `z'.$this->target['domainid'].'_mails` AS b WHERE a.`id` = b.`userid`) WHERE a.`id` = \''.$this->sql->escape_string($this->target['target']).'\'');
  161. } catch(Exception $e) {
  162. // ignore it
  163. }
  164. }
  165. function processRemote(&$txn) {
  166. $res = $this->runProtections($txn);
  167. if (!is_null($res)) return $res;
  168. // store mail & queue
  169. $store = $this->makeUniq('mailqueue');
  170. // store file
  171. $out = fopen($store, 'w');
  172. if (isset($this->target['extra_headers'])) {
  173. foreach($this->target['extra_headers'] as $h)
  174. fputs($out, Mail::header($h[0], $h[1]));
  175. }
  176. fputs($out, Mail::header('Received', '(PMaild '.getmypid().' invoked for remote email '.$this->target['mail'].'); '.date(DATE_RFC2822)));
  177. rewind($txn['fd']);
  178. stream_copy_to_stream($txn['fd'], $out);
  179. fclose($out);
  180. $insert = array(
  181. 'mlid' => basename($store),
  182. 'to' => $this->target['target'],
  183. 'queued' => $this->sql->now(),
  184. );
  185. if ($this->from !== '') $insert['from'] = $this->from;
  186. if (isset($this->target['tracker'])) $insert['tracker'] = $this->target['tracker'];
  187. $DAO = $this->sql->DAO('mailqueue', array('mlid', 'to'));
  188. if ($DAO->insertValues($insert)) return null;
  189. @unlink($store);
  190. Logger::log(Logger::LOG_ERR, $this->sql->error);
  191. return '400 4.0.0 Database error while queueing item';
  192. }
  193. function processList(&$txn) {
  194. $res = $this->runProtections($txn);
  195. if (!is_null($res)) return $res;
  196. if ($this->target['mode'] != 'message') return '400 4.5.0 This target is not implemented yet';
  197. // get targets list
  198. $DAO_lists_members = $this->sql->DAO('z'.$this->target['domainid'].'_lists_members', 'id');
  199. $DAO = $this->sql->DAO('mailqueue', array('mlid', 'to'));
  200. $list = $DAO_lists_members->loadByField(array('list_id' => $this->target['target'], 'status' => 'valid'));
  201. if (!$list) return NULL; // success (no target => nothing to do => we managed to do nothing => success)
  202. $got_first = false;
  203. foreach($list as $target) {
  204. $store = $this->makeUniq('mailqueue');
  205. if ($got_first === false) {
  206. $store = $this->makeUniq('mailqueue');
  207. $got_first = $store;
  208. $out = fopen($store, 'w');
  209. if (isset($this->target['extra_headers'])) {
  210. foreach($this->target['extra_headers'] as $h)
  211. fputs($out, Mail::header($h[0], $h[1]));
  212. }
  213. if ($this->from !== '') fputs($out, Mail::header('X-Original-From', $this->from));
  214. fputs($out, Mail::header('Received', '(PMaild '.getmypid().' invoked for list '.$this->target['target'].'); '.date(DATE_RFC2822)));
  215. rewind($txn['fd']);
  216. stream_copy_to_stream($txn['fd'], $out);
  217. fclose($out);
  218. } else {
  219. $store = $this->makeUniq('mailqueue');
  220. link($got_first, $store);
  221. }
  222. // drop "from" as it could be used to probe mailing list members, and we don't want to handle bounces
  223. $insert = array(
  224. 'mlid' => basename($store),
  225. 'to' => $target->email,
  226. 'queued' => $this->sql->now(),
  227. );
  228. if ($DAO->insertValues($insert)) continue;
  229. // failed?
  230. if ($store == $got_first) $got_first = false;
  231. @unlink($store);
  232. Logger::log(Logger::LOG_ERR, $this->sql->error);
  233. }
  234. if ($got_first === false) { // all failed
  235. return '400 4.0.0 Failed to transmit mail to list';
  236. }
  237. return NULL;
  238. }
  239. function processRedirect(&$txn) {
  240. return $this->processRemote($txn);
  241. }
  242. function httpAnswer($str) {
  243. if ($str[0]=='2') return null;
  244. return $str;
  245. }
  246. function handleHTTPAPI(&$txn, $call) {
  247. if (is_string($call)) return $this->httpAnswer($call);
  248. if (!is_array($call)) {
  249. if (isset($this->target['on_error'])) {
  250. $msg = 'While calling '.$url.":\nObject is not a string or an array\n\n".print_r($call, true);
  251. mail($this->target['on_error'], 'Error at '.$param['to'], $msg, 'From: "Mail Script Caller" <nobody@example.com>');
  252. }
  253. return '400 4.0.0 Problem with received object';
  254. }
  255. try {
  256. switch($call['action']) {
  257. case 'redirect':
  258. $this->target['type'] = 'remote';
  259. $this->target['target'] = $call['target'];
  260. if (isset($call['headers'])) {
  261. foreach($call['headers'] as $h)
  262. $this->target['extra_headers'][] = $h;
  263. }
  264. if (isset($call['tracker'])) {
  265. $this->target['tracker'] = $call['tracker'];
  266. $this->from = ''; // tracker => do not use "from" anymore?
  267. }
  268. return $this->process($txn); // reinject mail into system
  269. default:
  270. throw new Exception('Unknown call action');
  271. }
  272. } catch(Exception $e) {
  273. if (isset($this->target['on_error'])) {
  274. $msg = 'While calling '.$url.":\n".$e->getMessage()."\n\n".print_r($call, true);
  275. mail($this->target['on_error'], 'Error at '.$param['to'], $msg, 'From: "Mail Script Caller" <nobody@example.com>');
  276. }
  277. return '400 4.0.0 Problem with received object';
  278. }
  279. }
  280. // Forward email to an HTTP addr
  281. function processHttp(&$txn) {
  282. $url = $this->target['target'];
  283. if ($url[0] == '!') {
  284. $res = $this->runProtections($txn);
  285. if (!is_null($res)) return $res;
  286. $url = substr($url, 1);
  287. }
  288. $c = '?';
  289. if (strpos($url, '?') !== false) $c = '&';
  290. $param = array(
  291. // 'helo' => $txn['helo'],
  292. 'remote_ip' => $txn['peer'][0],
  293. 'remote_host' => $txn['peer'][2],
  294. 'from' => $this->from,
  295. 'to' => $this->target['mail'],
  296. 'api_version' => '2.0',
  297. );
  298. foreach($txn as $var=>$val) {
  299. if ($var == 'peer') continue;
  300. if ($var == 'fd') continue;
  301. if ($var == 'parent') continue;
  302. if (!is_string($val)) {
  303. $var = '_' . $var;
  304. $val = serialize($val);
  305. }
  306. $param[$var] = $val; // provide helo, clam and spamassassin infos (and other if available)
  307. }
  308. foreach($param as $var => $val) {
  309. $url .= $c . $var . '=' . urlencode($val);
  310. $c = '&';
  311. }
  312. fseek($txn['fd'], 0, SEEK_END);
  313. $len = ftell($txn['fd']);
  314. rewind($txn['fd']);
  315. $ch = \curl_init($url); // HTTP
  316. \curl_setopt($ch, CURLOPT_PUT, true);
  317. \curl_setopt($ch, CURLOPT_INFILE, $txn['fd']);
  318. \curl_setopt($ch, CURLOPT_INFILESIZE, $len);
  319. \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  320. \curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-Ipc: MAIL'));
  321. $res = \curl_exec($ch);
  322. if (substr($res, 0, 3) == '250') return null;
  323. if (!preg_match("/^[0-9]{3} [^\n]+\$/", $res)) {
  324. // check if that is an API call...
  325. if (substr($res, 0, 8) == 'PMAPI2.0') {
  326. $call = unserialize(substr($res, 8));
  327. if (!$call) {
  328. if (isset($this->target['on_error'])) {
  329. $msg = 'While calling '.$url.":\n\n".$res;
  330. mail($this->target['on_error'], 'Error at '.$param['to'], $msg, 'From: "Mail Script Caller" <nobody@example.com>');
  331. }
  332. return '450 4.0.0 Remote error while transferring mail, please retry later';
  333. }
  334. return $this->handleHTTPAPI($txn, $call);
  335. }
  336. if (isset($this->target['on_error'])) {
  337. $msg = 'While calling '.$url.":\n\n".$res;
  338. mail($this->target['on_error'], 'Error at '.$param['to'], $msg, 'From: "Mail Script Caller" <nobody@example.com>');
  339. }
  340. return '450 4.0.0 Remote error while transferring mail, please retry later';
  341. }
  342. return $this->httpAnswer($res);
  343. }
  344. function process(&$txn) {
  345. $func = 'process'.ucfirst($this->target['type']);
  346. return $this->$func($txn);
  347. }
  348. }