PageRenderTime 207ms CodeModel.GetById 61ms app.highlight 65ms RepoModel.GetById 75ms app.codeStats 1ms

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