<?php
/**
* @version	$Id: email_events_event_handler.php 15065 2012-01-17 15:11:29Z 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 EmailEventsEventsHandler extends kDBEventHandler
	{
		/**
		 * Allows to override standard permission mapping
		 *
		 * @return void
		 * @access protected
		 * @see kEventHandler::$permMapping
		 */
		protected function mapPermissions()
		{
			parent::mapPermissions();

			$permissions = Array (
				'OnFrontOnly' => Array ('self' => 'edit'),
				'OnSaveSelected' => Array ('self' => 'view'),
				'OnProcessEmailQueue' => Array ('self' => 'add|edit'),

				'OnSuggestAddress' => Array ('self' => 'add|edit'),

				// events only for developers
				'OnPreCreate' => Array ('self' => 'debug'),
				'OnDelete' => Array ('self' => 'debug'),
				'OnDeleteAll' => Array ('self' => 'debug'),
				'OnMassDelete' => Array ('self' => 'debug'),
				'OnMassApprove' => Array ('self' => 'debug'),
				'OnMassDecline' => Array ('self' => 'debug'),
			);

			$this->permMapping = array_merge($this->permMapping, $permissions);
		}

		/**
		 * Changes permission section to one from REQUEST, not from config
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access public
		 */
		public function CheckPermission(kEvent &$event)
		{
			$module = $this->Application->GetVar('module');

			if ( strlen($module) > 0 ) {
				// checking permission when lising module email events in separate section
				$module = explode(':', $module, 2);

				if ( count($module) == 1 ) {
					$main_prefix = $this->Application->findModule('Name', $module[0], 'Var');
				}
				else {
					$exceptions = Array ('Category' => 'c', 'Users' => 'u');
					$main_prefix = $exceptions[$module[1]];
				}
				$section = $this->Application->getUnitOption($main_prefix . '.email', 'PermSection');

				$event->setEventParam('PermSection', $section);
			}

			// checking permission when listing all email events when editing language
			return parent::CheckPermission($event);
		}

		/**
		 * Apply any custom changes to list's sql query
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 * @see kDBEventHandler::OnListBuild()
		 */
		protected function SetCustomQuery(kEvent &$event)
		{
			parent::SetCustomQuery($event);

			$object =& $event->getObject();
			/* @var $object kDBList */

			if ( $event->Special == 'module' ) {
				$module = $this->Application->GetVar('module');
				$object->addFilter('module_filter', '%1$s.Module = ' . $this->Conn->qstr($module));
			}

			if ( !$event->Special && !$this->Application->isDebugMode() ) {
				// no special
				$object->addFilter('enabled_filter', '%1$s.Enabled <> ' . STATUS_DISABLED);
			}
		}

		/**
		 * Prepares new kDBItem object
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnNew(kEvent &$event)
		{
			parent::OnNew($event);

			$mapping = Array ('conf' => 'VariableValue', 'site-domain' => 'DefaultEmailRecipients');

			if ( isset($mapping[$event->Special]) ) {
				$object =& $event->getObject();
				/* @var $object kDBItem */

				$target_object =& $this->Application->recallObject($event->Special);
				/* @var $target_object kDBList */

				$object->SetDBField('Recipients', $target_object->GetDBField($mapping[$event->Special]));
			}
		}

		/**
		 * Set default headers
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnPreCreate(kEvent &$event)
		{
			parent::OnPreCreate($event);

			$object =& $event->getObject();
			/* @var $object kDBItem */

			$object->SetDBField('Headers', $this->Application->ConfigValue('Smtp_DefaultHeaders'));
		}

		/**
		 * Sets status Front-End Only to selected email events
		 *
		 * @param kEvent $event
		 */
		function OnFrontOnly(&$event)
		{
			if ($this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1)) {
				$event->status = kEvent::erFAIL;
				return ;
			}

			$ids = implode(',', $this->StoreSelectedIDs($event));

			$table_name = $this->Application->getUnitOption($event->Prefix, 'TableName');
			$sql = 'UPDATE '.$table_name.'
					SET FrontEndOnly = 1
					WHERE EventId IN ('.$ids.')';
			$this->Conn->Query($sql);

			$this->clearSelectedIDs($event);
		}

		/**
		 * Sets selected user to email events selected
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnSelectUser(kEvent &$event)
		{
			if ( $event->Special != 'module' ) {
				parent::OnSelectUser($event);
				return;
			}

			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return;
			}

			$items_info = $this->Application->GetVar('u');
			if ( $items_info ) {
				$user_id = array_shift(array_keys($items_info));

				$selected_ids = $this->getSelectedIDs($event, true);

				$ids = $this->Application->RecallVar($event->getPrefixSpecial() . '_selected_ids');
				$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
				$table_name = $this->Application->getUnitOption($event->Prefix, 'TableName');

				$sql = 'UPDATE ' . $table_name . '
						SET ' . $this->Application->RecallVar('dst_field') . ' = ' . $user_id . '
						WHERE ' . $id_field . ' IN (' . $ids . ')';
				$this->Conn->Query($sql);
			}

			$this->finalizePopup($event);
		}

		/**
		 * Saves selected ids to session
		 *
		 * @param kEvent $event
		 */
		function OnSaveSelected(&$event)
		{
			$this->StoreSelectedIDs($event);
		}

		/**
		 * Returns email event object based on given kEvent object
		 *
		 * @param kEvent $event
		 * @return kDBItem
		 */
		function &_getEmailEvent(&$event)
		{
			$false = false;
			$name = $event->getEventParam('EmailEventName');
			$type = $event->getEventParam('EmailEventType');

			$object =& $event->getObject( Array('skip_autoload' => true) );
			/* @var $object kDBItem */

			if (!$object->isLoaded() || ($object->GetDBField('Event') != $name || $object->GetDBField('Type') != $type)) {
				// get event parameters by name & type
				$load_keys = Array ('Event' => $name, 'Type' => $type);
				$object->Load($load_keys);

				if (!$object->isLoaded() || ($object->GetDBField('Enabled') == STATUS_DISABLED)) {
					// event record not found OR is disabled
					return $false;
				}

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

			return $object;
		}

		/**
		 * Processes email sender
		 *
		 * @param kEvent $event
		 * @param Array $direct_params
		 */
		function _processSender(&$event, $direct_params = Array ())
		{
			$this->Application->removeObject('u.email-from');

			$object =& $this->_getEmailEvent($event);
			/* @var $object kDBItem */

			$email = $name = '';

			// set defaults from event
			if ($object->GetDBField('CustomSender')) {
				$address = $object->GetDBField('SenderAddress');
				$address_type = $object->GetDBField('SenderAddressType');

				switch ($address_type) {
					case EmailEvent::ADDRESS_TYPE_EMAIL:
						$email = $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
							$email = $user_info['Email'];
							$name = 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 ($object->GetDBField('SenderName')) {
					$name = $object->GetDBField('SenderName');
				}
			}

			// update with custom data given during event execution
			if (array_key_exists('from_email', $direct_params)) {
				$email = $direct_params['from_email'];
			}

			if (array_key_exists('from_name', $direct_params)) {
				$name = $direct_params['from_name'];
			}

			// still nothing, set defaults
			if (!$email) {
				$email = $this->Application->ConfigValue('DefaultEmailSender');
			}

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

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

			$esender->SetFrom($email, $name);

			return Array ($email, $name);
		}

		/**
		 * Processes email recipients
		 *
		 * @param kEvent $event
		 * @param Array $direct_params
		 * @return Array
		 */
		function _processRecipients(&$event, &$direct_params = Array ())
		{
			$this->Application->removeObject('u.email-to');

			$object =& $this->_getEmailEvent($event);
			/* @var $object kDBItem */

			$to_email = $to_name = '';
			$all_recipients = Array ();

			$this->_addRecipientsFromXml($all_recipients, $object->GetDBField('Recipients'));

			if ( !array_key_exists(EmailEvent::RECIPIENT_TYPE_TO, $all_recipients) ) {
				$all_recipients[EmailEvent::RECIPIENT_TYPE_TO] = Array ();
			}

			// remove all "To" recipients, when not allowed
			$overwrite_to_email = array_key_exists('overwrite_to_email', $direct_params) ? $direct_params['overwrite_to_email'] : false;

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

			// update with custom data given during event execution (user_id)
			$to_user_id = $event->getEventParam('EmailEventToUserId');

			if ( $to_user_id > 0 ) {
				$language_field = $event->getEventParam('EmailEventType') == EmailEvent::EVENT_TYPE_FRONTEND ? 'FrontLanguage' : 'AdminLanguage';

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

				if ( $user_info ) {
					$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($direct_params['language_id']) ) {
						$direct_params['language_id'] = $user_info['Language'];
					}

					array_unshift($all_recipients[EmailEvent::RECIPIENT_TYPE_TO], $add_recipient);

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

					$user->Load($to_user_id);
				}
			}
			elseif ( is_numeric($to_user_id) ) {
				// recipient is system user with negative ID (root, guest, etc.) -> send to admin
				$this->_addDefaultRecipient($all_recipients);
			}

			// update with custom data given during event execution (email + name)
			$add_recipient = Array ();

			if ( array_key_exists('to_email', $direct_params) ) {
				$add_recipient['RecipientName'] = '';
				$add_recipient['RecipientAddressType'] = EmailEvent::ADDRESS_TYPE_EMAIL;
				$add_recipient['RecipientAddress'] = $direct_params['to_email'];
			}

			if ( array_key_exists('to_name', $direct_params) ) {
				$add_recipient['RecipientName'] = $direct_params['to_name'];
			}

			if ( $add_recipient ) {
				array_unshift($all_recipients[EmailEvent::RECIPIENT_TYPE_TO], $add_recipient);
			}

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

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

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

			$default_email = $this->Application->ConfigValue('DefaultEmailSender');

			foreach ($all_recipients as $recipient_type => $recipients) {
				// add recipients to email
				$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;
					}
				}

				if ( !$pairs ) {
					continue;
				}

				if ( $recipient_type == EmailEvent::RECIPIENT_TYPE_TO ) {
					$to_email = $pairs[0]['email'] ? $pairs[0]['email'] : $default_email;
					$to_name = $pairs[0]['name'] ? $pairs[0]['name'] : $to_email;
				}

				$header_name = $header_mapping[$recipient_type];

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

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

			return Array ($to_email, $to_name);
		}

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

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

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

		/**
		 * Adds multiple recipients from an XML
		 *
		 * @param Array $recipients
		 * @param string $xml
		 * @return bool
		 * @access protected
		 */
		protected function _addRecipientsFromXml(&$recipients, $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) {
				$recipient_type = $record['RecipientType'];

				if ( !array_key_exists($recipient_type, $recipients) ) {
					$recipients[$recipient_type] = Array ();
				}

				$recipients[$recipient_type][] = $record;
			}

			return true;
		}

		/**
		 * Returns email event message by ID (headers & body in one piece)
		 *
		 * @param kEvent $event
		 * @param int $language_id
		 * @return string
		 */
		function _getMessageBody(&$event, $language_id = null)
		{
			if (!isset($language_id)) {
				$language_id = $this->Application->GetVar('m_lang');
			}

			$object =& $this->_getEmailEvent($event);

			// 1. get message body
			$message_body = $this->_formMessageBody($object, $language_id, $object->GetDBField('MessageType'));

			// 2. replace tags if needed
			$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 = $object->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) {
				$message_body = str_replace($replace_from, $replace_to, $message_body);
			}

			return $message_body;
		}

		/**
		 * Prepare email message body
		 *
		 * @param kDBItem $object
		 * @param int $language_id
		 * @return string
		 */
		function _formMessageBody(&$object, $language_id)
		{
			$default_language_id = $this->Application->GetDefaultLanguageId();

			$fields_hash = Array (
				'Headers' => $object->GetDBField('Headers'),
			);

			// prepare subject
			$subject = $object->GetDBField('l' . $language_id . '_Subject');

			if (!$subject) {
				$subject = $object->GetDBField('l' . $default_language_id . '_Subject');
			}

			$fields_hash['Subject'] = $subject;

			// prepare body
			$body = $object->GetDBField('l' . $language_id . '_Body');

			if (!$body) {
				$body = $object->GetDBField('l' . $default_language_id . '_Body');
			}

			$fields_hash['Body'] = $body;

			$email_message_helper =& $this->Application->recallObject('EmailMessageHelper');
			/* @var $email_message_helper EmailMessageHelper */

			$ret = $email_message_helper->buildTemplate($fields_hash);

			// add footer
			$footer = $this->_getFooter($language_id, $object->GetDBField('MessageType'));

			if ($ret && $footer) {
				$ret .= "\r\n" . $footer;
			}

			return $ret;
		}

		/**
		 * Returns email footer
		 *
		 * @param int $language_id
		 * @param string $message_type
		 * @return string
		 */
		function _getFooter($language_id, $message_type)
		{
			static $footer = null;

			if (!isset($footer)) {
				$default_language_id = $this->Application->GetDefaultLanguageId();

				$sql = 'SELECT l' . $language_id . '_Body, l' . $default_language_id . '_Body
	           			FROM ' . $this->Application->getUnitOption('emailevents', 'TableName') . ' em
	           			WHERE Event = "COMMON.FOOTER"';
				$footer_data = $this->Conn->GetRow($sql);

				$footer = $footer_data['l' . $language_id . '_Body'];

				if (!$footer) {
					$footer = $footer_data['l' . $default_language_id . '_Body'];
				}

				if ($message_type == 'text') {
					$esender =& $this->Application->recallObject('EmailSender');
					/* @var $esender kEmailSendingHelper */

					$footer = $esender->ConvertToText($footer);
				}
			}

			return $footer;
		}

		/**
		 * Parse message template and return headers (as array) and message body part
		 *
		 * @param string $message
		 * @param Array $direct_params
		 * @return Array
		 */
		function ParseMessageBody($message, $direct_params = Array ())
		{
			$message_language = $this->_getSendLanguage($direct_params);
			$this->_changeLanguage($message_language);

			$direct_params['message_text'] = isset($direct_params['message']) ? $direct_params['message'] : ''; // parameter alias

			// 1. parse template
			$this->Application->InitParser();
			$parser_params = $this->Application->Parser->Params; // backup parser params

			$this->Application->Parser->SetParams( array_merge($parser_params, $direct_params) );

			$message = implode('&|&', explode("\n\n", $message, 2)); // preserves double \n in case when tag is located in subject field
			$message = $this->Application->Parser->Parse($message, 'email_template', 0);

			$this->Application->Parser->SetParams($parser_params); // restore parser params

			// 2. replace line endings, that are send with data submitted via request
			$message = str_replace("\r\n", "\n", $message); // possible case
			$message = str_replace("\r", "\n", $message); // impossible case, but just in case replace this too

			// 3. separate headers from body
			$message_headers = Array ();
			list($headers, $message_body) = explode('&|&', $message, 2);

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

			$message_body = $category_helper->replacePageIds($message_body);

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

			$this->_changeLanguage();
			return Array ($message_headers, $message_body);
		}

		/**
		 * Raised when email message should be sent
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnEmailEvent(&$event)
		{
			$email_event_name = $event->getEventParam('EmailEventName');
			if ( strpos($email_event_name, '_') !== false ) {
				throw new Exception('<span class="debug_error">Invalid email event name</span> <strong>' . $email_event_name . '</strong>. Use only <strong>UPPERCASE characters</strong> and <strong>dots</strong> as email event names');
			}

			$object =& $this->_getEmailEvent($event);

			if ( !is_object($object) ) {
				// email event not found OR it's won't be send under given circumstances
				return ;
			}

			// additional parameters from kApplication->EmailEvent
			$send_params = $event->getEventParam('DirectSendParams');

			// 1. get information about message sender and recipient
			list ($from_email, $from_name) = $this->_processSender($event, $send_params);
			list ($to_email, $to_name) = $this->_processRecipients($event, $send_params);

			// 2. prepare message to be sent
			$message_language = $this->_getSendLanguage($send_params);
			$message_template = $this->_getMessageBody($event, $message_language);
			if ( !trim($message_template) ) {
				trigger_error('Message template is empty', E_USER_WARNING);
				return ;
			}

			list ($message_headers, $message_body) = $this->ParseMessageBody($message_template, $send_params);
			if ( !trim($message_body) ) {
				trigger_error('Message template is empty after parsing', E_USER_WARNING);
				return ;
			}

			// 3. set headers & send message
			$esender =& $this->Application->recallObject('EmailSender');
			/* @var $esender kEmailSendingHelper */

			$message_subject = isset($message_headers['Subject']) ? $message_headers['Subject'] : 'Mail message';
			$esender->SetSubject($message_subject);

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

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

			$esender->CreateTextHtmlPart($message_body, $object->GetDBField('MessageType') == 'html');

			$log_fields_hash = Array (
				'fromuser'		=> $from_name . ' (' . $from_email . ')',
				'addressto'		=> $to_name . ' (' . $to_email . ')',
				'subject'		=>	$message_subject,
				'timestamp'		=>	adodb_mktime(),
				'event'			=>	$email_event_name,
				'EventParams'	=>	serialize( $this->removeSendingParams($send_params) ),
			);

			$esender->setLogData($log_fields_hash);
			$event->status = $esender->Deliver() ? kEvent::erSUCCESS : kEvent::erFAIL;
		}

		/**
		 * Removes parameters, used during e-mail sending
		 *
		 * @param Array $params
		 * @return Array
		 * @access protected
		 */
		protected function removeSendingParams($params)
		{
			$send_keys = Array ('from_email', 'from_name', 'to_email', 'to_name', 'message');

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

			return $params;
		}

		function _getSendLanguage($send_params)
		{
			if (array_key_exists('language_id', $send_params)) {
				return $send_params['language_id'];
			}

			return $this->Application->GetVar('m_lang');
		}

		function _changeLanguage($language_id = null)
		{
			static $prev_language_id = null;

			if ( !isset($language_id) ) {
				// restore language
				$language_id = $prev_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 ();

			$prev_language_id = $language_id; // for restoring it later
		}

		/**
		 * Process emails from queue
		 *
		 * @param kEvent $event
		 * @todo Move to MailingList
		 */
		function OnProcessEmailQueue(&$event)
		{
			$deliver_count = $event->getEventParam('deliver_count');
			if ($deliver_count === false) {
				$deliver_count = $this->Application->ConfigValue('MailingListSendPerStep');
				if ($deliver_count === false) {
					$deliver_count = 10; // 10 emails per script run (if not specified directly)
				}
			}

			$processing_type = $this->Application->GetVar('type');
			if ($processing_type = 'return_progress') {
				$email_queue_progress = $this->Application->RecallVar('email_queue_progress');
				if ($email_queue_progress === false) {
					$emails_sent = 0;
					$sql = 'SELECT COUNT(*)
							FROM ' . TABLE_PREFIX . 'EmailQueue
							WHERE (SendRetries < 5) AND (LastSendRetry < ' . strtotime('-2 hours') . ')';
					$total_emails = $this->Conn->GetOne($sql);
					$this->Application->StoreVar('email_queue_progress', $emails_sent.':'.$total_emails);
				}
				else {
					list ($emails_sent, $total_emails) = explode(':', $email_queue_progress);
				}
			}

			$sql = 'SELECT *
					FROM '.TABLE_PREFIX.'EmailQueue
					WHERE (SendRetries < 5) AND (LastSendRetry < ' . strtotime('-2 hours') . ')
					LIMIT 0,' . $deliver_count;
			$messages = $this->Conn->Query($sql);

			$message_count = count($messages);
			if (!$message_count) {
				// no messages left to send in queue
				if ($processing_type = 'return_progress') {
					$this->Application->RemoveVar('email_queue_progress');
					$this->Application->Redirect($this->Application->GetVar('finish_template'));
				}
				return ;
			}

			$mailing_list_helper =& $this->Application->recallObject('MailingListHelper');
			/* @var $mailing_list_helper MailingListHelper */

			$mailing_list_helper->processQueue($messages);

			if ($processing_type = 'return_progress') {
				$emails_sent += $message_count;
				if ($emails_sent >= $total_emails) {
					$this->Application->RemoveVar('email_queue_progress');
					$this->Application->Redirect($this->Application->GetVar('finish_template'));
				}

				$this->Application->StoreVar('email_queue_progress', $emails_sent.':'.$total_emails);
				$event->status = kEvent::erSTOP;
				echo ($emails_sent / $total_emails) * 100;
			}
		}

		/**
		 * Prefills module dropdown
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterConfigRead(kEvent &$event)
		{
			parent::OnAfterConfigRead($event);

			$options = Array ();

			foreach ($this->Application->ModuleInfo as $module_name => $module_info) {
				if ($module_name == 'In-Portal') {
					continue;
				}

				$options[$module_name] = $module_name;
			}

			$fields = $this->Application->getUnitOption($event->Prefix, 'Fields');
			$fields['Module']['options'] = $options;
			$this->Application->setUnitOption($event->Prefix, 'Fields', $fields);

			if ($this->Application->GetVar('regional')) {
				$this->Application->setUnitOption($event->Prefix, 'PopulateMlFields', true);
			}
		}

		/**
		 * Prepare temp tables and populate it
		 * with items selected in the grid
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnEdit(kEvent &$event)
		{
			parent::OnEdit($event);

			// use language from grid, instead of primary language used by default
			$event->SetRedirectParam('m_lang', $this->Application->GetVar('m_lang'));
		}

		/**
		 * Fixes default recipient type
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnAfterItemLoad(kEvent &$event)
		{
			parent::OnAfterItemLoad($event);

			$object =& $event->getObject();
			/* @var $object kDBItem */

			if (!$this->Application->isDebugMode(false)) {
				if ($object->GetDBField('AllowChangingRecipient')) {
					$object->SetDBField('RecipientType', EmailEvent::RECIPIENT_TYPE_TO);
				}
				else {
					$object->SetDBField('RecipientType', EmailEvent::RECIPIENT_TYPE_CC);
				}
			}

			// process replacement tags
			$records = Array ();
			$replacement_tags = $object->GetDBField('ReplacementTags');
			$replacement_tags = $replacement_tags ? unserialize($replacement_tags) : Array ();

			foreach ($replacement_tags as $tag => $replacement) {
				$records[] = Array ('Tag' => $tag, 'Replacement' => $replacement);
			}

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

			$xml = $minput_helper->prepareMInputXML($records, Array ('Tag', 'Replacement'));
			$object->SetDBField('ReplacementTagsXML', $xml);
		}

		/**
		 * Performs custom validation + keep read-only fields
		 *
		 * @param kEvent $event
		 */
		function _itemChanged(&$event)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			// validate email subject and body for parsing errors
			$this->_validateEmailTemplate($object);

			// validate sender and recipient addresses
			if ($object->GetDBField('CustomSender')) {
				$this->_validateAddress($event, 'Sender');
			}
			$this->_validateAddress($event, 'Recipient');

			if (!$this->Application->isDebugMode(false)) {
				// only allow to enable/disable event while in debug mode
				$to_restore = Array ('Enabled', 'AllowChangingSender', 'AllowChangingRecipient');

				if (!$object->GetOriginalField('AllowChangingSender')) {
					$to_restore = array_merge($to_restore, Array ('CustomSender', 'SenderName', 'SenderAddressType', 'SenderAddress'));
				}

				if (!$object->GetOriginalField('AllowChangingRecipient')) {
					$to_restore = array_merge($to_restore, Array ('CustomRecipient'/*, 'Recipients'*/));
				}

				// prevent specific fields from editing
				foreach ($to_restore as $restore_field) {
					$original_value = $object->GetOriginalField($restore_field);

					if ($object->GetDBField($restore_field) != $original_value) {
						$object->SetDBField($restore_field, $original_value);
					}
				}
			}

			// process replacement tags
			if ( $object->GetDBField('ReplacementTagsXML') ) {
				$minput_helper =& $this->Application->recallObject('MInputHelper');
				/* @var $minput_helper MInputHelper */

				$replacement_tags = Array ();
				$records = $minput_helper->parseMInputXML( $object->GetDBField('ReplacementTagsXML') );

				foreach ($records as $record) {
					$replacement_tags[ trim($record['Tag']) ] = trim($record['Replacement']);
				}

				$object->SetDBField('ReplacementTags', $replacement_tags ? serialize($replacement_tags) : NULL);
			}
		}

		/**
		 * Validates address using given field prefix
		 *
		 * @param kEvent $event
		 * @param string $field_prefix
		 */
		function _validateAddress(&$event, $field_prefix)
		{
			$object =& $event->getObject();
			/* @var $object kDBItem */

			$address_type = $object->GetDBField($field_prefix . 'AddressType');
			$object->setRequired($field_prefix . 'Address', $address_type > 0);
			$address = $object->GetDBField($field_prefix . 'Address');

			if (!$address) {
				// don't validate against empty address
				return ;
			}

			switch ($address_type) {
				case EmailEvent::ADDRESS_TYPE_EMAIL:
					if (!preg_match('/^(' . REGEX_EMAIL_USER . '@' . REGEX_EMAIL_DOMAIN . ')$/i', $address)) {
						$object->SetError($field_prefix . 'Address', 'invalid_email');
					}
					break;

				case EmailEvent::ADDRESS_TYPE_USER:
					$sql = 'SELECT PortalUserId
							FROM ' . TABLE_PREFIX . 'Users
							WHERE Username = ' . $this->Conn->qstr($address);
					if (!$this->Conn->GetOne($sql)) {
						$object->SetError($field_prefix . 'Address', 'invalid_user');
					}
					break;

				case EmailEvent::ADDRESS_TYPE_GROUP:
					$sql = 'SELECT GroupId
							FROM ' . TABLE_PREFIX . 'UserGroups
							WHERE Name = ' . $this->Conn->qstr($address);
					if (!$this->Conn->GetOne($sql)) {
						$object->SetError($field_prefix . 'Address', 'invalid_group');
					}
					break;
			}
		}

		/**
		 * Don't allow to enable/disable events in non-debug mode
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemCreate(kEvent &$event)
		{
			parent::OnBeforeItemCreate($event);

			$this->_itemChanged($event);
		}

		/**
		 * Don't allow to enable/disable events in non-debug mode
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemUpdate(kEvent &$event)
		{
			parent::OnBeforeItemUpdate($event);

			$this->_itemChanged($event);
		}

		/**
		 * Suggest address based on typed address and selected address type
		 *
		 * @param kEvent $event
		 */
		function OnSuggestAddress(&$event)
		{
			$event->status = kEvent::erSTOP;

			$address_type = $this->Application->GetVar('type');
			$address = $this->Application->GetVar('value');
			$limit = $this->Application->GetVar('limit');

			if ( !$limit ) {
				$limit = 20;
			}

			switch ($address_type) {
				case EmailEvent::ADDRESS_TYPE_EMAIL:
					$field = 'Email';
					$table_name = TABLE_PREFIX . 'Users';
					break;

				case EmailEvent::ADDRESS_TYPE_USER:
					$field = 'Username';
					$table_name = TABLE_PREFIX . 'Users';
					break;

				case EmailEvent::ADDRESS_TYPE_GROUP:
					$field = 'Name';
					$table_name = TABLE_PREFIX . 'UserGroups';
					break;

				default:
					$field = $table_name = '';
					break;
			}

			if ( $field ) {
				$sql = 'SELECT DISTINCT ' . $field . '
						FROM ' . $table_name . '
						WHERE ' . $field . ' LIKE ' . $this->Conn->qstr($address . '%') . '
						ORDER BY ' . $field . ' ASC
						LIMIT 0,' . $limit;
				$data = $this->Conn->GetCol($sql);
			}
			else {
				$data = Array ();
			}

			$this->Application->XMLHeader();

			echo '<suggestions>';

			foreach ($data as $item) {
				echo '<item>' . htmlspecialchars($item) . '</item>';
			}

			echo '</suggestions>';
		}

		/**
		 * Validates subject and body fields of Email template
		 * @param kDBItem $object
		 */
		function _validateEmailTemplate(&$object)
		{
			$this->parseField($object, 'Subject');
			$this->parseField($object, 'Body');
		}

		/**
		 * Parses contents of given object field and sets error, when invalid in-portal tags found
		 * @param kDBItem $object
		 * @param string $field
		 * @return void
		 */
		function parseField(&$object, $field)
		{
			$this->Application->InitParser();

			try {
				$this->Application->Parser->CompileRaw($object->GetField($field), 'email_template');
			}
			catch (ParserException $e) {
				if ( $this->Application->isDebugMode() ) {
					$this->Application->Debugger->appendHTML('<b style="color: red;">Error in Email Template:</b> ' . $e->getMessage() . ' (line: ' . $e->getLine() . ')');
				}

				$object->SetError($field, 'parsing_error');
			}
		}
	}