<?php
/**
* @version	$Id: email_events_event_handler.php 14241 2011-03-16 20:24:35Z 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 standart permission mapping
		 *
		 */
		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
		 */
		function CheckPermission(&$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
		 * @access protected
		 * @see OnListBuild
		 */
		function 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);
			}
		}

		/**
		 * Sets event id
		 *
		 * @param kEvent $event
		 */
		function OnPreCreate(&$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 = 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
		 */
		function OnSelectUser(&$event)
		{
			if ($event->Special != 'module') {
				parent::OnSelectUser($event);
				return ;
			}

			if ($this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1)) {
				$event->status = 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 ADDRESS_TYPE_EMAIL:
						$email = $address;
						break;

					case ADDRESS_TYPE_USER:
						$sql = 'SELECT FirstName, LastName, Email, PortalUserId
								FROM ' . TABLE_PREFIX . 'PortalUser
								WHERE Login = ' . $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('Smtp_AdminMailFrom');
			}

			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
		 */
		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 ();
			$recipients_xml = $object->GetDBField('Recipients');

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

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

				foreach ($records as $record) {
					$recipient_type = $record['RecipientType'];

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

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

			if (!array_key_exists(RECIPIENT_TYPE_TO, $all_recipients)) {
				$all_recipients[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[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) {
				$sql = 'SELECT FirstName, LastName, Email
						FROM ' . TABLE_PREFIX . 'PortalUser
						WHERE PortalUserId = ' . $to_user_id;
				$user_info = $this->Conn->GetRow($sql);

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

					array_unshift($all_recipients[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
				array_unshift($all_recipients[RECIPIENT_TYPE_TO], $this->_getDefaultRepipient());
			}

			// 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'] = 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[RECIPIENT_TYPE_TO], $add_recipient);
			}

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

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

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

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

			foreach ($all_recipients as $recipient_type => $recipients) {
				// add recipients to email
				$pairs = Array ();

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

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

						case ADDRESS_TYPE_USER:
							$sql = 'SELECT FirstName, LastName, Email
									FROM ' . TABLE_PREFIX . 'PortalUser
									WHERE Login = ' . $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 : $repipient_name,
								);
							}
							break;

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

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

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

				if (!$pairs) {
					continue;
				}

				if ($recipient_type == 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
		 *
		 * @return Array
		 */
		function _getDefaultRepipient()
		{
			return Array (
				'RecipientName' => $this->Application->ConfigValue('Smtp_AdminMailFrom'),
				'RecipientAddressType' => ADDRESS_TYPE_EMAIL,
				'RecipientAddress' => $this->Application->ConfigValue('Smtp_AdminMailFrom'),
			);
		}

		/**
		 * Returns email event message by ID (headers & body in one piece)
		 *
		 * @param kEvent $event
		 * @param int $language_id
		 */
		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="Login"',
				'<inp:touser _Field' => '<inp2:u_Field name',
			);

			$replacement_tags = $object->GetDBField('ReplacementTags');
			$replacement_tags = $replacement_tags ? unserialize($replacement_tags) : Array ();
			$replacement_tags = array_merge_recursive2($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_recursive2($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 shoul be sent
		 *
		 * @param kEvent $event
		 */
		function OnEmailEvent(&$event)
		{
			$email_event_name = $event->getEventParam('EmailEventName');
			if (strpos($email_event_name, '_') !== false) {
				trigger_error('<span class="debug_error">Invalid email event name</span> <b>'.$email_event_name.'</b>. Use only <b>UPPERCASE characters</b> and <b>dots</b> as email event names', E_USER_ERROR);
			}

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

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

			// 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 false;
			}

			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 false;
			}

			// 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 determite what's actually was received
				$message_headers['X-Event-Name'] = $email_event_name . ' - ' . ($object->GetDBField('Type') == 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');

			$event->status = $esender->Deliver() ? erSUCCESS : erFAIL;

			if ($event->status == erSUCCESS) {
				// all keys, that are not used in email sending are written to log record
				$send_keys = Array ('from_email', 'from_name', 'to_email', 'to_name', 'message');
				foreach ($send_keys as $send_key) {
					unset($send_params[$send_key]);
				}

				$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($send_params),
				);

				$this->Conn->doInsert($fields_hash, TABLE_PREFIX.'EmailLog');
			}
		}

		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 $lang_object kDBItem */

			$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 = erSTOP;
				echo ($emails_sent / $total_emails) * 100;
			}
		}

		/**
		 * Prefills module dropdown
		 *
		 * @param kEvent $event
		 */
		function OnAfterConfigRead(&$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
		 */
		function OnEdit(&$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
		 */
		function OnAfterItemLoad(&$event)
		{
			parent::OnAfterItemLoad($event);

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

			if (!$this->Application->isDebugMode(false)) {
				if ($object->GetDBField('AllowChangingRecipient')) {
					$object->SetDBField('RecipientType', RECIPIENT_TYPE_TO);
				}
				else {
					$object->SetDBField('RecipientType', 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 ADDRESS_TYPE_EMAIL:
					if (!preg_match('/^(' . REGEX_EMAIL_USER . '@' . REGEX_EMAIL_DOMAIN . ')$/i', $address)) {
						$object->SetError($field_prefix . 'Address', 'invalid_email');
					}
					break;

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

				case ADDRESS_TYPE_GROUP:
					$sql = 'SELECT GroupId
							FROM ' . TABLE_PREFIX . 'PortalGroup
							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
		 */
		function OnBeforeItemCreate(&$event)
		{
			parent::OnBeforeItemCreate($event);

			$this->_itemChanged($event);
		}

		/**
		 * Don't allow to enable/disable events in non-debug mode
		 *
		 * @param kEvent $event
		 */
		function OnBeforeItemUpdate(&$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 = 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 ADDRESS_TYPE_EMAIL:
					$field = 'Email';
					$table_name = TABLE_PREFIX . 'PortalUser';
					break;

				case ADDRESS_TYPE_USER:
					$field = 'Login';
					$table_name = TABLE_PREFIX . 'PortalUser';
					break;

				case ADDRESS_TYPE_GROUP:
					$field = 'Name';
					$table_name = TABLE_PREFIX . 'PortalGroup';
					break;
			}

			if (isset($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)
		{
			// 1. prepare parser and error handler
			safeDefine('DBG_IGNORE_FATAL_ERRORS', 1);

			$error_handlers = $this->Application->errorHandlers;
			$this->Application->errorHandlers = Array (
				Array(&$this, '_saveError'),
			);

			$this->Application->InitParser();
			$this->Application->SetVar('email_parsing_error', 0);

			// 2. parse subject
			$this->Application->Parser->CompileRaw($object->GetField('Subject'), 'email_subject');

			if ( $this->Application->GetVar('email_parsing_error') ) {
				$object->SetError('Subject', 'parsing_error');
				$this->Application->SetVar('email_parsing_error', 0); // reset the error status
			}

			// 3. parse body
			$this->Application->Parser->CompileRaw($object->GetField('Body'), 'email_template');

			if ( $this->Application->GetVar('email_parsing_error') ) {
				$object->SetError('Body', 'parsing_error');
			}

			// 4. restore original settings
			$this->Application->errorHandlers = $error_handlers;
		}

		/**
		 * Custom error handler that saves errors during email validation
		 * @param string $errno
		 * @param string $errstr
		 * @param string $errfile
		 * @param string $errline
		 * @param string $errcontext
		 */
		function _saveError($errno, $errstr, $errfile, $errline, $errcontext)
		{
			if ($errno != E_USER_ERROR) {
				// ignore all minor errors, except fatals from parser
				return true;
			}

			$this->Application->SetVar('email_parsing_error', 1);

			if ( $this->Application->isDebugMode() ) {
				$this->Application->Debugger->appendHTML('<b style="color: red;">Error in Email Template:</b> ' . $errstr . ' (line: ' . $errline . ')');
			}

			return true;
		}
	}