<?php
/**
* @version	$Id: mailbox_helper.php 15137 2012-03-04 08:06:21Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class MailboxHelper extends kHelper {

		var $headers = Array ();

		var $parsedMessage = Array ();

		/**
		 * Maximal megabytes of data to process
		 *
		 * @var int
		 */
		var $maxMegabytes = 2;

		/**
		 * Maximal message count to process
		 *
		 * @var int
		 */
		var $maxMessages = 50;

		/**
		 * Reads mailbox and gives messages to processing callback
		 *
		 * @param Array $connection_info
		 * @param Array $verify_callback
		 * @param Array $process_callback
		 * @param Array $callback_params
		 * @param bool $include_attachment_contents
		 * @return string
		 */
		function process($connection_info, $verify_callback, $process_callback, $callback_params = Array (), $include_attachment_contents = true)
		{
			$pop3_helper = $this->Application->makeClass('POP3Helper', Array ($connection_info));
			/* @var $pop3_helper POP3Helper */

			$connection_status = $pop3_helper->initMailbox();

			if (is_string($connection_status)) {
				return $connection_status;
			}

			if (defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode()) {
				$this->Application->Debugger->appendHTML('Reading MAILBOX: ' . $connection_info['username']);
			}

			// Figure out if all messages are huge
			$only_big_messages = true;
			$max_message_size = $this->maxMegabytes * (1024 * 1024);

			foreach ($pop3_helper->messageSizes as $message_size) {
				if (($message_size <= $max_message_size) && ($max_message_size > 0)) {
					$only_big_messages = false;
					break;
				}
			}

			$count = $total_size = 0;

			foreach ($pop3_helper->messageSizes as $message_number => $message_size) {
				// Too many messages?
				if (($count++ > $this->maxMessages) && ($this->maxMessages > 0)) {
					break;
				}

				// Message too big?
				if (!$only_big_messages && ($message_size > $max_message_size) && ($max_message_size > 0)) {
					$this->_displayLogMessage('message <strong>#' . $message_number . '</strong> too big, skipped');
					continue;
				}

				// Processed enough for today?
				if (($total_size > $max_message_size) && ($max_message_size > 0)) {
					break;
				}

				$total_size += $message_size;
				$pop3_helper->getEmail($message_number, $message_source);

				$processed = $this->normalize($message_source, $verify_callback, $process_callback, $callback_params, $include_attachment_contents);

				if ($processed) {
					// delete message from server immediatly after retrieving & processing
					$pop3_helper->deleteEmail($message_number);
					$this->_displayLogMessage('message <strong>#' . $message_number . '</strong>: processed');
				}
				else {
					$this->_displayLogMessage('message <strong>#' . $message_number . '</strong>: skipped');
				}
			}

			$pop3_helper->close();

			return 'success';
		}

		/**
		 * Displays log message
		 *
		 * @param string $text
		 */
		function _displayLogMessage($text)
		{
			if (defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode()) {
				$this->Application->Debugger->appendHTML($text);
			}
		}

		/**
		 * Takes an RFC822 formatted date, returns a unix timestamp (allowing for zone)
		 *
		 * @param string $rfcdate
		 * @return int
		 */
		function rfcToTime($rfcdate)
		{
			$date = strtotime($rfcdate);

			if ($date == -1) {
				return false;
			}

			return $date;
		}

		/**
		 * Gets recipients from all possible headers
		 *
		 * @return string
		 */
		function getRecipients()
		{
			$ret = '';

			// headers that could contain recipients
			$recipient_headers = Array (
				'to', 'cc', 'envelope-to', 'resent-to', 'delivered-to',
				'apparently-to', 'envelope-to', 'x-envelope-to', 'received',
			);

			foreach ($recipient_headers as $recipient_header) {
				if (!array_key_exists($recipient_header, $this->headers)) {
					continue;
				}

				if (!is_array($this->headers["$recipient_header"])) {
					$ret .= ' ' . $this->headers["$recipient_header"];
				} else {
					$ret .= ' ' . implode(' ', $this->headers["$recipient_header"]);
				}
			}

			return $ret;
		}

		/**
		 * "Flattens" the multi-demensinal headers array into a single dimension one
		 *
		 * @param Array $input
		 * @param string $add
		 * @return Array
		 */
		function flattenHeadersArray($input, $add = '')
		{
			$output = Array ();

			foreach ($input as $key => $value) {
				if (!empty($add)) {
					$newkey = ucfirst( strtolower($add) );
				} elseif (is_numeric($key)) {
					$newkey = '';
				} else {
					$newkey = ucfirst( strtolower($key) );
				}

				if (is_array($value)) {
					$output = array_merge($output, $this->flattenHeadersArray($value, $newkey));
				} else {
					$output[] = (!empty($newkey) ? $newkey . ': ' : '') . $value;
				}
			}

			return $output;
		}

		/**
		 * Processes given message using given callbacks
		 *
		 * @param string $message
		 * @param Array $verify_callback
		 * @param Array $process_callback
		 * @param Array $callback_params
		 * @param bool $include_attachment_contents
		 * @return bool
		 * @access protected
		 */
		protected function normalize($message, $verify_callback, $process_callback, $callback_params, $include_attachment_contents = true)
		{
			// Decode message
			$this->decodeMime($message, $include_attachment_contents);

			// Init vars; $good will hold all the correct infomation from now on
			$good = Array ();

			// trim() some stuff now instead of later
			$this->headers['from'] = trim($this->headers['from']);
			$this->headers['to'] = trim($this->headers['to']);
			$this->headers['cc'] = array_key_exists('cc', $this->headers) ? trim($this->headers['cc']) : '';
			$this->headers['x-forward-to'] = array_key_exists('x-forward-to', $this->headers) ? $this->headers['x-forward-to'] : '';
			$this->headers['subject'] = trim($this->headers['subject']);
			$this->headers['received'] = is_array($this->headers['received']) ? $this->headers['received'] : Array ($this->headers['received']);

			if (array_key_exists('return-path', $this->headers) && is_array($this->headers['return-path'])) {
				$this->headers['return-path'] = implode(' ', $this->flattenHeadersArray($this->headers['return-path']));
			}

			// Create our own message-ID if it's missing
			$message_id = array_key_exists('message-id', $this->headers) ? trim($this->headers['message-id']) : '';
			$good['emailid'] = $message_id ? $message_id : md5($message) . "@in-portal";

			// Stops us looping in stupid conversations with other mail software
			if (isset($this->headers['x-loop-detect']) && $this->headers['x-loop-detect'] > 2) {
				return false;
			}

			$esender = $this->Application->recallObject('EmailSender');
			/* @var $esender kEmailSendingHelper */

			// Get the return address
			$return_path = '';

			if (array_key_exists('return-path', $this->headers)) {
				$return_path = $esender->ExtractRecipientEmail($this->headers['return-path']);
			}

			if (!$return_path) {
				if (array_key_exists('reply-to', $this->headers)) {
					$return_path = $esender->ExtractRecipientEmail( $this->headers['reply-to'] );
				}
				else {
					$return_path = $esender->ExtractRecipientEmail( $this->headers['from'] );
				}
			}

			// Get the sender's name & email
			$good['fromemail'] = $esender->ExtractRecipientEmail($this->headers['from']);
			$good['fromname'] = $esender->ExtractRecipientName($this->headers['from'], $good['fromemail']);

			// Get the list of recipients
			if (!$verify_callback[0]->$verify_callback[1]($callback_params)) {
				// error: mail is propably spam
				return false;
			}

			// Handle the subject
			$good['subject'] = $this->headers['subject'];

			// Priorities rock
			$good['priority'] = array_key_exists('x-priority', $this->headers) ? (int)$this->headers['x-priority'] : 0;

			switch ($good['priority']) {
				case 1: case 5: break;
				default:
					$good['priority'] = 3;
			}

			// If we have attachments it's about time we tell the user about it
			if (array_key_exists('attachments', $this->parsedMessage) && is_array($this->parsedMessage['attachments'])) {
				$good['attach'] = count( $this->parsedMessage['attachments'] );
			} else {
				$good['attach'] = 0;
			}

			// prepare message text (for replies, etc)
			if (isset($this->parsedMessage['text'][0]) && trim($this->parsedMessage['text'][0]['body']) != '') {
				$message_body = trim($this->parsedMessage['text'][0]['body']);
				$message_type = 'text';
			} elseif (isset($this->parsedMessage['html']) && trim($this->parsedMessage['html'][0]['body']) != '') {
				$message_body = trim($this->parsedMessage['html'][0]['body']);
				$message_type = 'html';
			} else {
				$message_body = '[no message]';
				$message_type = 'text';
			}

			// remove scripts
			$message_body = preg_replace("/<script[^>]*>[^<]+<\/script[^>]*>/is", '', $message_body);
			$message_body = preg_replace("/<iframe[^>]*>[^<]*<\/iframe[^>]*>/is", '', $message_body);

			if ($message_type == 'html') {
				$message_body = $esender->ConvertToText($message_body);
			}

			$mime_decode_helper = $this->Application->recallObject('MimeDecodeHelper');
			/* @var $mime_decode_helper MimeDecodeHelper */

			// convert to site encoding
			$message_charset = $this->parsedMessage[$message_type][0]['charset'];

			if ($message_charset) {
				$good['message'] = $mime_decode_helper->convertEncoding($message_charset, $message_body);
			}

			if (array_key_exists('delivery-date', $this->headers)) {
				// We found the Delivery-Date header (and it's not too far in the future)
				$dateline = $this->rfcToTime($this->headers['delivery-date']);

				if ($dateline > TIMENOW + 86400) {
					unset($dateline);
				}
			}

			// We found the latest date from the received headers
			$received_timestamp = $this->headers['received'][0];
			$dateline = $this->rfcToTime(trim( substr($received_timestamp, strrpos($received_timestamp, ';') + 1) ));

			if ($dateline == $this->rfcToTime(0)) {
				unset($dateline);
			}

			if (!isset($dateline)) {
				$dateline = TIMENOW;
			}

			// save collected data to database
			$fields_hash = Array (
				'DeliveryDate' 		=> $dateline, // date, when SMTP server received the message
				'ReceivedDate' 		=> TIMENOW, // date, when message was retrieved from POP3 server
				'CreatedOn' 		=> $this->rfcToTime($this->headers['date']), // date, when created on sender's computer
				'ReturnPath'		=> $return_path,
				'FromEmail'			=> $good['fromemail'],
				'FromName'			=> $good['fromname'],
				'To'				=> $this->headers['to'],
				'Subject'			=> $good['subject'],
				'Message'			=> $good['message'],
				'MessageType'		=> $message_type,
				'AttachmentCount'	=> $good['attach'],
				'MessageId'			=> $good['emailid'],
				'Source'			=> $message,
				'Priority'			=> $good['priority'],
				'Size'				=> strlen($message),
			);

			return $process_callback[0]->$process_callback[1]($callback_params, $fields_hash);
		}

		/**
		 * Function that decodes the MIME message and creates the $this->headers and $this->parsedMessage data arrays
		 *
		 * @param string $message
		 * @param bool $include_attachments
		 *
		 */
		function decodeMime($message, $include_attachments = true)
		{
			$message = preg_replace("/\r?\n/", "\r\n", trim($message));

			$mime_decode_helper = $this->Application->recallObject('MimeDecodeHelper');
			/* @var $mime_decode_helper MimeDecodeHelper */

			// 1. separate headers from bodies
			$mime_decode_helper->InitHelper($message);
			$decoded_message = $mime_decode_helper->decode(true, true, true);

			// 2. extract attachments
			$this->parsedMessage = Array (); // ! reset value
			$this->parseOutput($decoded_message, $this->parsedMessage, $include_attachments);

			// 3. add "other" attachments (text part, that is not maked as attachment)
			if (array_key_exists('text', $this->parsedMessage) && count($this->parsedMessage['text']) > 1) {
				for ($attach = 1; $attach < count($this->parsedMessage['text']); $attach++) {
					$this->parsedMessage['attachments'][] = Array (
						'data' => $this->parsedMessage['text']["$attach"]['body'],
					);
				}
			}

			$this->headers = $this->parsedMessage['headers']; // ! reset value

			if (empty($decoded_message->ctype_parameters['boundary'])) {
				// when no boundary, then assume all message is it's text
				$this->parsedMessage['text'][0]['body'] = $decoded_message->body;
			}
		}

		/**
		 * Returns content-id's from inline attachments in message
		 *
		 * @return Array
		 */
		function getContentIds()
		{
			$cids = Array();

			if (array_key_exists('attachments', $this->parsedMessage) && is_array($this->parsedMessage['attachments'])) {
				foreach ($this->parsedMessage['attachments'] as $attachnum => $attachment) {
					if (!isset($attachment['headers']['content-id'])) {
						continue;
					}

					$cid = $attachment['headers']['content-id'];

					if (substr($cid, 0, 1) == '<' && substr($cid, -1) == '>') {
						$cid = substr($cid, 1, -1);
					}

					$cids["$attachnum"] = $cid;
				}
			}

			return $cids;
		}

		/**
		 * Get more detailed information about attachments
		 *
		 * @param stdClass $decoded parsed headers & body as object
		 * @param Array $parts parsed parts
		 * @param bool $include_attachments
		 */
		function parseOutput(&$decoded, &$parts, $include_attachments = true)
		{
			$ctype = strtolower($decoded->ctype_primary . '/' . $decoded->ctype_secondary);

			// don't parse attached messages recursevely
			if (!empty($decoded->parts) && $ctype != 'message/rfc822') {
				for ($i = 0; $i < count($decoded->parts); $i++) {
					$this->parseOutput($decoded->parts["$i"], $parts, $include_attachments);
				}
			} else/*if (!empty($decoded->disposition) && $decoded->disposition != 'inline' or 1)*/ {
				switch ($ctype) {
					case 'text/plain':
					case 'text/html':
						if (!empty($decoded->disposition) && ($decoded->disposition == 'attachment')) {
							$parts['attachments'][] = Array (
								'data' => $include_attachments ? $decoded->body : '',
								'filename' => array_key_exists('filename', $decoded->d_parameters) ? $decoded->d_parameters['filename'] : '', // from content-disposition
								'filename2' => $decoded->ctype_parameters['name'], // from content-type
								'type' => $decoded->ctype_primary, // "text"
								'encoding' => $decoded->headers['content-transfer-encoding']
							);
						} else {
							$body_type = $decoded->ctype_secondary == 'plain' ? 'text' : 'html';
							$parts[$body_type][] = Array (
								'content-type' => $ctype,
								'charset' => array_key_exists('charset', $decoded->ctype_parameters) ? $decoded->ctype_parameters['charset'] : 'ISO-8859-1',
								'body' => $decoded->body
							);
						}
						break;

					case 'message/rfc822':
						// another e-mail as attachment
						$parts['attachments'][] = Array (
							'data' => $include_attachments ? $decoded->body : '',
							'filename' => array_key_exists('filename', $decoded->d_parameters) ? $decoded->d_parameters['filename'] : '',
							'filename2' => array_key_exists('name', $decoded->ctype_parameters) ? $decoded->ctype_parameters['name'] : $decoded->parts[0]->headers['subject'],
							'type' => $decoded->ctype_primary, // "message"
							'headers' => $decoded->headers // individual copy of headers with each attachment
						);
						break;

					default:
						if (!stristr($decoded->headers['content-type'], 'signature')) {
							$parts['attachments'][] = Array (
								'data' => $include_attachments ? $decoded->body : '',
								'filename' => array_key_exists('filename', $decoded->d_parameters) ? $decoded->d_parameters['filename'] : '', // from content-disposition
								'filename2' => $decoded->ctype_parameters['name'], // from content-type
								'type' => $decoded->ctype_primary,
								'headers' => $decoded->headers // individual copy of headers with each attachment
							);
						}

				}
			}

			$parts['headers'] = $decoded->headers; // headers of next parts overwrite previous part headers
		}

	}