<?php
/**
 * @version	$Id: email.php 15248 2012-03-30 16:19:34Z alex $
 * @package	In-Portal
 * @copyright	Copyright (C) 1997 - 2012 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 kEmail extends kBase {

	/**
	 * Reference to class, that could send out e-mail
	 *
	 * @var kEmailSendingHelper
	 * @access protected
	 */
	protected $sender = null;

	/**
	 * Parameters of e-mail
	 *
	 * @var Array
	 * @access protected
	 * @see kEmail::_getCustomParams() List of possible system parameters supported
	 */
	protected $params = Array ();

	/**
	 * Reference to e-mail event object, that would be used as data source
	 *
	 * @var kDBItem
	 */
	protected $emailEvent = null;

	/**
	 * Sender name
	 *
	 * @var string
	 * @access protected
	 */
	protected $fromName = '';

	/**
	 * Sender e-mail
	 *
	 * @var string
	 * @access protected
	 */
	protected $fromEmail = '';

	/**
	 * Recipient name
	 *
	 * @var string
	 * @access protected
	 */
	protected $toName = '';

	/**
	 * Recipient e-mail
	 *
	 * @var string
	 * @access protected
	 */
	protected $toEmail = '';

	/**
	 * ID of recipient user
	 *
	 * @var int
	 */
	protected $recipientUserId = null;

	/**
	 * List of e-mail recipients
	 *
	 * @var Array
	 * @access protected
	 */
	protected $recipients = Array (
		EmailEvent::RECIPIENT_TYPE_TO => Array (),
		EmailEvent::RECIPIENT_TYPE_CC => Array (),
		EmailEvent::RECIPIENT_TYPE_BCC => Array (),
	);

	/**
	 * Creates e-mail instance
	 */
	public function __construct()
	{
		parent::__construct();

		$this->sender = $this->Application->recallObject('EmailSender');
	}

	/**
	 * Resets state of e-mail
	 *
	 * @return void
	 * @access protected
	 */
	protected function _resetState()
	{
		$this->fromEmail = $this->fromName = '';
		$this->Application->removeObject('u.email-from');

		$this->recipients = Array (
			EmailEvent::RECIPIENT_TYPE_TO => Array (),
			EmailEvent::RECIPIENT_TYPE_CC => Array (),
			EmailEvent::RECIPIENT_TYPE_BCC => Array (),
		);

		$this->toEmail = $this->toEmail = '';
		$this->Application->removeObject('u.email-to');
	}

	/**
	 * Finds e-mail event matching user data
	 *
	 * @param string $name
	 * @param int $type
	 * @return bool
	 * @throws InvalidArgumentException
	 * @access public
	 */
	public function findEvent($name, $type)
	{
		if ( !$name || !preg_match('/^[A-Z\.]+$/', $name) ) {
			throw new InvalidArgumentException('Invalid e-mail event name "<strong>' . $name . '</strong>". Only <strong>UPPERCASE characters</strong> and <strong>dots</strong> are allowed.');
		}

		if ( $type != EmailEvent::EVENT_TYPE_ADMIN && $type != EmailEvent::EVENT_TYPE_FRONTEND ) {
			throw new InvalidArgumentException('Invalid e-mail event type');
		}

		$this->emailEvent = $this->Application->recallObject('emailevents', null, Array ('skip_autoload' => true));

		if ( !$this->emailEvent->isLoaded() || !$this->_sameEvent($name, $type) ) {
			// get event parameters by name & type
			$this->emailEvent->Load(Array ('Event' => $name, 'Type' => $type));
		}

		return $this->_eventUsable();
	}

	/**
	 * Detects, that given event data matches currently used event
	 *
	 * @param string $name
	 * @param int $type
	 * @return bool
	 * @access protected
	 */
	protected function _sameEvent($name, $type)
	{
		return $this->emailEvent->GetDBField('Event') == $name && $this->emailEvent->GetDBField('Type') == $type;
	}

	/**
	 * Determines if we can use e-mail event we've found based on user data
	 *
	 * @return bool
	 * @access protected
	 */
	protected function _eventUsable()
	{
		if ( !$this->emailEvent->isLoaded() || $this->emailEvent->GetDBField('Enabled') == STATUS_DISABLED ) {
			return false;
		}

		if ( $this->emailEvent->GetDBField('FrontEndOnly') && $this->Application->isAdmin ) {
			return false;
		}

		return true;
	}

	/**
	 * Sets e-mail event params
	 *
	 * @param Array $params
	 * @access public
	 */
	public function setParams($params)
	{
		$this->params = $params;
	}

	/**
	 * Returns any custom parameters, that are passed when invoked e-mail event sending
	 *
	 * @return Array
	 * @access protected
	 */
	protected function _getCustomParams()
	{
		$ret = $this->params;
		$send_keys = Array ('from_email', 'from_name', 'to_email', 'to_name', 'overwrite_to_email', 'language_id', 'use_custom_design');

		foreach ($send_keys as $send_key) {
			unset($ret[$send_key]);
		}

		return $ret;
	}

	/**
	 * Sends e-mail now or puts it in queue
	 *
	 * @param int $recipient_user_id
	 * @param bool $immediate_send
	 * @return bool
	 * @access public
	 */
	public function send($recipient_user_id = null, $immediate_send = true)
	{
		$this->recipientUserId = $recipient_user_id;

		$this->_resetState();
		$this->_processSender();
		$this->_processRecipients();
		$this->_changeLanguage(false);

		// 1. set headers
		$message_headers = $this->_getHeaders();
		$message_subject = isset($message_headers['Subject']) ? $message_headers['Subject'] : 'Mail message';
		$this->sender->SetSubject($message_subject);

		foreach ($message_headers as $header_name => $header_value) {
			$this->sender->SetEncodedHeader($header_name, $header_value);
		}

		if ( $this->_storeEmailLog() ) {
			// 2. prepare log
			$log_fields_hash = Array (
				'From' => $this->fromName . ' (' . $this->fromEmail . ')',
				'To' => $this->toName . ' (' . $this->toEmail . ')',
				'OtherRecipients' => serialize($this->recipients),
				'Subject' => $message_subject,
				'SentOn' => TIMENOW,
				'EventName' => $this->emailEvent->GetDBField('Event'),
				'EventParams' => serialize($this->_getCustomParams()),
			);

			$this->params['email_access_key'] = $this->_generateAccessKey($log_fields_hash);
		}

		// 3. set body
		$html_message_body = $this->_getMessageBody(true);
		$plain_message_body = $this->_getMessageBody(false);

		if ( $html_message_body === false && $plain_message_body === false ) {
			trigger_error('Message template is empty (maybe after parsing).', E_USER_WARNING);

			return false;
		}

		if ( $html_message_body !== false ) {
			$this->sender->CreateTextHtmlPart($html_message_body, true);
		}

		if ( $plain_message_body !== false ) {
			$this->sender->CreateTextHtmlPart($plain_message_body, false);
		}

		$this->_changeLanguage(true);

		if ( $this->_storeEmailLog() ) {
			// 4. set log
			$log_fields_hash['HtmlBody'] = $html_message_body;
			$log_fields_hash['TextBody'] = $plain_message_body;
			$log_fields_hash['AccessKey'] = $this->params['email_access_key'];
			$this->sender->setLogData($log_fields_hash);
		}

		return $this->sender->Deliver(null, $immediate_send);
	}

	/**
	 * Determines whatever we should keep e-mail log or not
	 *
	 * @return bool
	 * @access protected
	 */
	protected function _storeEmailLog()
	{
		return $this->Application->ConfigValue('EmailLogRotationInterval') != '';
	}

	/**
	 * Generates access key for accessing e-mail later
	 *
	 * @param Array $log_fields_hash
	 * @return string
	 * @access protected
	 */
	protected function _generateAccessKey($log_fields_hash)
	{
		$ret = '';
		$use_fields = Array ('From', 'To', 'Subject');

		foreach ($use_fields as $use_field) {
			$ret .= $log_fields_hash[$use_field] . ':';
		}

		return md5($ret . microtime(true));
	}

	/**
	 * Processes email sender
	 *
	 * @return void
	 * @access protected
	 */
	protected function _processSender()
	{
		if ( $this->emailEvent->GetDBField('CustomSender') ) {
			$this->_processCustomSender();
		}

		// update with custom data given during event execution
		if ( isset($this->params['from_email']) ) {
			$this->fromEmail = $this->params['from_email'];
		}

		if ( isset($this->params['from_name']) ) {
			$this->fromName = $this->params['from_name'];
		}

		// still nothing, set defaults
		$this->_ensureDefaultSender();
		$this->sender->SetFrom($this->fromEmail, $this->fromName);
	}

	/**
	 * Processes custom e-mail sender
	 *
	 * @return void
	 * @access protected
	 */
	protected function _processCustomSender()
	{
		$address = $this->emailEvent->GetDBField('SenderAddress');
		$address_type = $this->emailEvent->GetDBField('SenderAddressType');

		switch ($address_type) {
			case EmailEvent::ADDRESS_TYPE_EMAIL:
				$this->fromEmail = $address;
				break;

			case EmailEvent::ADDRESS_TYPE_USER:
				$sql = 'SELECT FirstName, LastName, Email, PortalUserId
						FROM ' . TABLE_PREFIX . 'Users
						WHERE Username = ' . $this->Conn->qstr($address);
				$user_info = $this->Conn->GetRow($sql);

				if ( $user_info ) {
					// user still exists
					$this->fromEmail = $user_info['Email'];
					$this->fromName = trim($user_info['FirstName'] . ' ' . $user_info['LastName']);

					$user = $this->Application->recallObject('u.email-from', null, Array ('skip_autoload' => true));
					/* @var $user UsersItem */

					$user->Load($user_info['PortalUserId']);
				}
				break;
		}

		if ( $this->emailEvent->GetDBField('SenderName') ) {
			$this->fromName = $this->emailEvent->GetDBField('SenderName');
		}
	}

	/**
	 * Ensures, that sender name & e-mail are not empty
	 *
	 * @return void
	 * @access protected
	 */
	protected function _ensureDefaultSender()
	{
		if ( !$this->fromEmail ) {
			$this->fromEmail = $this->Application->ConfigValue('DefaultEmailSender');
		}

		if ( !$this->fromName ) {
			$this->fromName = strip_tags($this->Application->ConfigValue('Site_Name'));
		}
	}

	/**
	 * Processes email recipients
	 *
	 * @return void
	 * @access protected
	 */
	protected function _processRecipients()
	{
		$this->_collectRecipients();

		$header_mapping = Array (
			EmailEvent::RECIPIENT_TYPE_TO => 'To',
			EmailEvent::RECIPIENT_TYPE_CC => 'Cc',
			EmailEvent::RECIPIENT_TYPE_BCC => 'Bcc',
		);

		$default_email = $this->Application->ConfigValue('DefaultEmailSender');
		$this->recipients = array_map(Array ($this, '_transformRecipientsIntoPairs'), $this->recipients);

		foreach ($this->recipients as $recipient_type => $recipients) {
			// add recipients to email
			if ( !$recipients ) {
				continue;
			}

			if ( $recipient_type == EmailEvent::RECIPIENT_TYPE_TO ) {
				$this->toEmail = $recipients[0]['email'] ? $recipients[0]['email'] : $default_email;
				$this->toName = $recipients[0]['name'] ? $recipients[0]['name'] : $this->toEmail;
			}

			$header_name = $header_mapping[$recipient_type];

			foreach ($recipients as $recipient) {
				$email = $recipient['email'] ? $recipient['email'] : $default_email;
				$name = $recipient['name'] ? $recipient['name'] : $email;

				$this->sender->AddRecipient($header_name, $email, $name);
			}
		}
	}

	/**
	 * Collects e-mail recipients from various sources
	 *
	 * @return void
	 * @access protected
	 */
	protected function _collectRecipients()
	{
		$this->_addRecipientsFromXml($this->emailEvent->GetDBField('Recipients'));
		$this->_overwriteToRecipient();
		$this->_addRecipientByUserId();
		$this->_addRecipientFromParams();

		if ( ($this->emailEvent->GetDBField('Type') == EmailEvent::EVENT_TYPE_ADMIN) && !$this->recipients[EmailEvent::RECIPIENT_TYPE_TO] ) {
			// admin email event without direct recipient -> send to admin
			$this->_addDefaultRecipient();
		}
	}

	/**
	 * Adds multiple recipients from an XML
	 *
	 * @param string $xml
	 * @return bool
	 * @access protected
	 */
	protected function _addRecipientsFromXml($xml)
	{
		if ( !$xml ) {
			return false;
		}

		$minput_helper = $this->Application->recallObject('MInputHelper');
		/* @var $minput_helper MInputHelper */

		// group recipients by type
		$records = $minput_helper->parseMInputXML($xml);

		foreach ($records as $record) {
			$this->recipients[$record['RecipientType']][] = $record;
		}

		return true;
	}

	/**
	 * Remove all "To" recipients, when not allowed
	 *
	 * @return void
	 * @access protected
	 */
	protected function _overwriteToRecipient()
	{
		$overwrite_to_email = isset($this->params['overwrite_to_email']) ? $this->params['overwrite_to_email'] : false;

		if ( !$this->emailEvent->GetDBField('CustomRecipient') || $overwrite_to_email ) {
			$this->recipients[EmailEvent::RECIPIENT_TYPE_TO] = Array ();
		}
	}

	/**
	 * Update with custom data given during event execution (user_id)
	 *
	 * @return void
	 * @access protected
	 */
	protected function _addRecipientByUserId()
	{
		if ( !is_numeric($this->recipientUserId) ) {
			return;
		}

		if ( $this->recipientUserId <= 0 ) {
			// recipient is system user with negative ID (root, guest, etc.) -> send to admin
			$this->_addDefaultRecipient();

			return;
		}

		$language_field = $this->emailEvent->GetDBField('Type') == EmailEvent::EVENT_TYPE_FRONTEND ? 'FrontLanguage' : 'AdminLanguage';

		$sql = 'SELECT FirstName, LastName, Email, ' . $language_field . ' AS Language
				FROM ' . TABLE_PREFIX . 'Users
				WHERE PortalUserId = ' . $this->recipientUserId;
		$user_info = $this->Conn->GetRow($sql);

		if ( !$user_info ) {
			return;
		}

		$add_recipient = Array (
			'RecipientAddressType' => EmailEvent::ADDRESS_TYPE_EMAIL,
			'RecipientAddress' => $user_info['Email'],
			'RecipientName' => trim($user_info['FirstName'] . ' ' . $user_info['LastName']),
		);

		if ( $user_info['Language'] && !isset($this->params['language_id']) ) {
			$this->params['language_id'] = $user_info['Language'];
		}

		array_unshift($this->recipients[EmailEvent::RECIPIENT_TYPE_TO], $add_recipient);

		$user = $this->Application->recallObject('u.email-to', null, Array('skip_autoload' => true));
		/* @var $user UsersItem */

		$user->Load($this->recipientUserId);
	}

	/**
	 * Update with custom data given during event execution (email + name)
	 *
	 * @return void
	 * @access protected
	 */
	protected function _addRecipientFromParams()
	{
		$add_recipient = Array ();

		if ( isset($this->params['to_email']) && $this->params['to_email'] ) {
			$add_recipient['RecipientName'] = '';
			$add_recipient['RecipientAddressType'] = EmailEvent::ADDRESS_TYPE_EMAIL;
			$add_recipient['RecipientAddress'] = $this->params['to_email'];
		}

		if ( isset($this->params['to_name']) && $this->params['to_name'] ) {
			$add_recipient['RecipientName'] = $this->params['to_name'];
		}

		if ( $add_recipient ) {
			array_unshift($this->recipients[EmailEvent::RECIPIENT_TYPE_TO], $add_recipient);
		}
	}

	/**
	 * This is default recipient, when we can't determine actual one
	 *
	 * @return void
	 * @access protected
	 */
	protected function _addDefaultRecipient()
	{
		$xml = $this->Application->ConfigValue('DefaultEmailRecipients');

		if ( !$this->_addRecipientsFromXml($xml) ) {
			$recipient = Array (
				'RecipientName' => $this->Application->ConfigValue('DefaultEmailSender'),
				'RecipientAddressType' => EmailEvent::ADDRESS_TYPE_EMAIL,
				'RecipientAddress' => $this->Application->ConfigValue('DefaultEmailSender'),
			);

			array_unshift($this->recipients[EmailEvent::RECIPIENT_TYPE_TO], $recipient);
		}
	}

	/**
	 * Transforms recipients into name/e-mail pairs
	 *
	 * @param Array $recipients
	 * @return Array
	 * @access protected
	 */
	protected function _transformRecipientsIntoPairs($recipients)
	{
		if ( !$recipients ) {
			return Array ();
		}

		$pairs = Array ();

		foreach ($recipients as $recipient) {
			$address = $recipient['RecipientAddress'];
			$address_type = $recipient['RecipientAddressType'];
			$recipient_name = $recipient['RecipientName'];

			switch ($address_type) {
				case EmailEvent::ADDRESS_TYPE_EMAIL:
					$pairs[] = Array ('email' => $address, 'name' => $recipient_name);
					break;

				case EmailEvent::ADDRESS_TYPE_USER:
					$sql = 'SELECT FirstName, LastName, Email
							FROM ' . TABLE_PREFIX . 'Users
							WHERE Username = ' . $this->Conn->qstr($address);
					$user_info = $this->Conn->GetRow($sql);

					if ( $user_info ) {
						// user still exists
						$name = trim($user_info['FirstName'] . ' ' . $user_info['LastName']);

						$pairs[] = Array (
							'email' => $user_info['Email'],
							'name' => $name ? $name : $recipient_name,
						);
					}
					break;

				case EmailEvent::ADDRESS_TYPE_GROUP:
					$sql = 'SELECT u.FirstName, u.LastName, u.Email
							FROM ' . TABLE_PREFIX . 'UserGroups g
							JOIN ' . TABLE_PREFIX . 'UserGroupRelations ug ON ug.GroupId = g.GroupId
							JOIN ' . TABLE_PREFIX . 'Users u ON u.PortalUserId = ug.PortalUserId
							WHERE g.Name = ' . $this->Conn->qstr($address);
					$users = $this->Conn->Query($sql);

					foreach ($users as $user_info) {
						$name = trim($user_info['FirstName'] . ' ' . $user_info['LastName']);

						$pairs[] = Array (
							'email' => $user_info['Email'],
							'name' => $name ? $name : $recipient_name,
						);
					}
					break;
			}
		}

		return $pairs;
	}
	/**
	 * Change system language temporarily to send e-mail on user language
	 *
	 * @param bool $restore
	 * @return void
	 * @access protected
	 */
	protected function _changeLanguage($restore = false)
	{
		static $prev_language_id = null;

		if ( !isset($prev_language_id) ) {
			$prev_language_id = $this->Application->GetVar('m_lang');
		}

		// ensure that language is set
		if ( !isset($this->params['language_id']) ) {
			$this->params['language_id'] = $this->Application->GetVar('m_lang');
		}

		$language_id = $restore ? $prev_language_id : $this->params['language_id'];
		$this->Application->SetVar('m_lang', $language_id);

		$language = $this->Application->recallObject('lang.current');
		/* @var $language LanguagesItem */

		$language->Load($language_id);

		$this->Application->Phrases->LanguageId = $language_id;
		$this->Application->Phrases->Phrases = Array ();
	}

	/**
	 * Parses message headers into array
	 *
	 * @return Array
	 * @access protected
	 */
	protected function _getHeaders()
	{
		$headers = $this->emailEvent->GetDBField('Headers');
		$headers = 'Subject: ' . $this->emailEvent->GetField('Subject') . ($headers ? "\n" . $headers : '');
		$headers = explode("\n", $this->_parseText($headers));

		$ret = Array ();

		foreach ($headers as $header) {
			$header = explode(':', $header, 2);
			$ret[ trim($header[0]) ] = trim($header[1]);
		}

		if ( $this->Application->isDebugMode() ) {
			// set special header with event name, so it will be easier to determine what's actually was received
			$event_type = $this->emailEvent->GetDBField('Type') == EmailEvent::EVENT_TYPE_ADMIN ? 'ADMIN' : 'USER';
			$ret['X-Event-Name'] = $this->emailEvent->GetDBField('Event') . ' - ' . $event_type;
		}

		return $ret;
	}

	/**
	 * Applies design to given e-mail text
	 *
	 * @param string $text
	 * @param bool $is_html
	 * @return string
	 * @access protected
	 * @todo maybe ConvertToText will strip all <inp2: tags
	 */
	protected function _applyMessageDesign($text, $is_html = true)
	{
		static $design_templates = Array();

		$design_key = $is_html ? 'html' : 'text';

		if ( !isset($design_templates[$design_key]) ) {
			$language = $this->Application->recallObject('lang.current');
			/* @var $language LanguagesItem */

			$design_template = $language->GetDBField($is_html ? 'HtmlEmailTemplate' : 'TextEmailTemplate');

			if ( !$is_html && !$design_template ) {
				$design_template = $this->sender->ConvertToText($language->GetDBField('HtmlEmailTemplate'));
			}

			$design_templates[$design_key] = $design_template;
		}

		return $this->_parseText(str_replace('$body', $text, $design_templates[$design_key]));
	}

	/**
	 * Returns message body
	 *
	 * @param bool $is_html
	 * @return bool|string
	 * @access protected
	 * @todo maybe ConvertToText will strip all <inp2: tags
	 */
	protected function _getMessageBody($is_html = false)
	{
		$message_body = $this->emailEvent->GetField($is_html ? 'HtmlBody' : 'PlainTextBody');

		if ( !trim($message_body) && !$is_html ) {
			// no plain text part available -> make it from html part then
			$message_body = $this->sender->ConvertToText($this->emailEvent->GetField('HtmlBody'));
		}

		if ( !trim($message_body) ) {
			return false;
		}

		if ( isset($this->params['use_custom_design']) && $this->params['use_custom_design'] ) {
			$message_body = $this->_parseText($message_body);
		}
		else {
			$message_body = $this->_applyMessageDesign($message_body, $is_html);
		}

		return trim($message_body) ? $message_body : false;
	}

	/**
	 * Parse message template and return headers (as array) and message body part
	 *
	 * @param string $text
	 * @return string
	 * @access protected
	 */
	protected function _parseText($text)
	{
		$text = $this->_substituteReplacementTags($text);

		if ( !$text ) {
			return '';
		}

		// init for cases, when e-mail is sent from event before page template rendering
		$this->Application->InitParser();

		$parser_params = $this->Application->Parser->Params; // backup parser params
		$this->Application->Parser->SetParams( array_merge($parser_params, $this->params) );

		$text = $this->Application->Parser->Parse($text, 'email_template');
		$this->Application->Parser->SetParams($parser_params); // restore parser params

		$category_helper = $this->Application->recallObject('CategoryHelper');
		/* @var $category_helper CategoryHelper */

		return $category_helper->replacePageIds($this->_normalizeLineEndings($text));
	}

	/**
	 * Substitutes replacement tags in given text
	 *
	 * @param string $text
	 * @return string
	 * @access protected
	 */
	protected function _substituteReplacementTags($text)
	{
		$default_replacement_tags = Array (
			'<inp:touser _Field="password"' => '<inp2:u_Field name="Password_plain"',
			'<inp:touser _Field="UserName"' => '<inp2:u_Field name="Username"',
			'<inp:touser _Field' => '<inp2:u_Field name',
		);

		$replacement_tags = $this->emailEvent->GetDBField('ReplacementTags');
		$replacement_tags = $replacement_tags ? unserialize($replacement_tags) : Array ();
		$replacement_tags = array_merge($default_replacement_tags, $replacement_tags);

		foreach ($replacement_tags as $replace_from => $replace_to) {
			$text = str_replace($replace_from, $replace_to, $text);
		}

		return $text;
	}

	/**
	 * Normalizes line endings in given text
	 *
	 * @param string $text
	 * @return string
	 * @access protected
	 */
	protected function _normalizeLineEndings($text)
	{
		// remove trailing line endings
		$text = preg_replace('/(\n|\r)+/', "\\1", $text);

		// convert Unix/Windows/Mac line ending into Unix line endings
		return str_replace(Array ("\r\n", "\r"), "\n", $text);
	}
}