Array (),
		EmailTemplate::RECIPIENT_TYPE_CC => Array (),
		EmailTemplate::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 (
			EmailTemplate::RECIPIENT_TYPE_TO => Array (),
			EmailTemplate::RECIPIENT_TYPE_CC => Array (),
			EmailTemplate::RECIPIENT_TYPE_BCC => Array (),
		);
		$this->toEmail = $this->toEmail = '';
		$this->Application->removeObject('u.email-to');
	}
	/**
	 * Finds e-mail template matching user data
	 *
	 * @param string $name
	 * @param int $type
	 * @return bool
	 * @throws InvalidArgumentException
	 * @access public
	 */
	public function findTemplate($name, $type)
	{
		if ( !$name || !preg_match('/^[A-Z\.]+$/', $name) ) {
			throw new InvalidArgumentException('Invalid e-mail template name "' . $name . '". Only UPPERCASE characters and dots are allowed.');
		}
		if ( $type != EmailTemplate::TEMPLATE_TYPE_ADMIN && $type != EmailTemplate::TEMPLATE_TYPE_FRONTEND ) {
			throw new InvalidArgumentException('Invalid e-mail template type');
		}
		// use "-item" special prevent error, when e-mail sent out from e-mail templates list
		$this->emailTemplate = $this->Application->recallObject('email-template.-item', null, Array ('skip_autoload' => true));
		if ( !$this->emailTemplate->isLoaded() || !$this->_sameTemplate($name, $type) ) {
			// get template parameters by name & type
			$this->emailTemplate->Load(Array ('TemplateName' => $name, 'Type' => $type));
		}
		return $this->_templateUsable();
	}
	/**
	 * Detects, that given e-mail template data matches currently used e-mail template
	 *
	 * @param string $name
	 * @param int $type
	 * @return bool
	 * @access protected
	 */
	protected function _sameTemplate($name, $type)
	{
		return $this->emailTemplate->GetDBField('TemplateName') == $name && $this->emailTemplate->GetDBField('Type') == $type;
	}
	/**
	 * Determines if we can use e-mail template we've found based on user data
	 *
	 * @return bool
	 * @access protected
	 */
	protected function _templateUsable()
	{
		if ( !$this->emailTemplate->isLoaded() || $this->emailTemplate->GetDBField('Enabled') == STATUS_DISABLED ) {
			return false;
		}
		if ( $this->emailTemplate->GetDBField('FrontEndOnly') && $this->Application->isAdmin ) {
			return false;
		}
		return true;
	}
	/**
	 * Sets e-mail template params
	 *
	 * @param Array $params
	 * @access public
	 */
	public function setParams($params)
	{
		$this->params = $params;
	}
	/**
	 * Returns any custom parameters, that are passed when invoked e-mail template 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', 'delivery');
		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
	 * @return bool
	 * @access public
	 */
	public function send($recipient_user_id = null)
	{
		$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,
				'TemplateName' => $this->emailTemplate->GetDBField('TemplateName'),
				'EventType' => $this->emailTemplate->GetDBField('Type'),
				'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);
		}
		$delivery = isset($this->params['delivery']) ? $this->params['delivery'] : $this->Application->ConfigValue('EmailDelivery');
		return $this->sender->Deliver(null, $delivery == EmailDelivery::IMMEDIATE);
	}
	/**
	 * Determines whatever we should keep e-mail log or not
	 *
	 * @return bool
	 * @access protected
	 */
	protected function _storeEmailLog()
	{
		return $this->Application->ConfigValue('EnableEmailLog');
	}
	/**
	 * 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->emailTemplate->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->emailTemplate->GetDBField('SenderAddress');
		$address_type = $this->emailTemplate->GetDBField('SenderAddressType');
		switch ($address_type) {
			case EmailTemplate::ADDRESS_TYPE_EMAIL:
				$this->fromEmail = $address;
				break;
			case EmailTemplate::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->emailTemplate->GetDBField('SenderName') ) {
			$this->fromName = $this->emailTemplate->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 (
			EmailTemplate::RECIPIENT_TYPE_TO => 'To',
			EmailTemplate::RECIPIENT_TYPE_CC => 'Cc',
			EmailTemplate::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 == EmailTemplate::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->emailTemplate->GetDBField('Recipients'));
		$this->_overwriteToRecipient();
		$this->_addRecipientByUserId();
		$this->_addRecipientFromParams();
		if ( ($this->emailTemplate->GetDBField('Type') == EmailTemplate::TEMPLATE_TYPE_ADMIN) && !$this->recipients[EmailTemplate::RECIPIENT_TYPE_TO] ) {
			// admin email template 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->emailTemplate->GetDBField('CustomRecipient') || $overwrite_to_email ) {
			$this->recipients[EmailTemplate::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->emailTemplate->GetDBField('Type') == EmailTemplate::TEMPLATE_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' => EmailTemplate::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[EmailTemplate::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'] = EmailTemplate::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[EmailTemplate::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' => EmailTemplate::ADDRESS_TYPE_EMAIL,
				'RecipientAddress' => $this->Application->ConfigValue('DefaultEmailSender'),
			);
			array_unshift($this->recipients[EmailTemplate::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 EmailTemplate::ADDRESS_TYPE_EMAIL:
					$pairs[] = Array ('email' => $address, 'name' => $recipient_name);
					break;
				case EmailTemplate::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 EmailTemplate::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->emailTemplate->GetDBField('Headers');
		$headers = 'Subject: ' . $this->emailTemplate->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 template name, so it will be easier to determine what's actually was received
			$template_type = $this->emailTemplate->GetDBField('Type') == EmailTemplate::TEMPLATE_TYPE_ADMIN ? 'ADMIN' : 'USER';
			$ret['X-Template-Name'] = $this->emailTemplate->GetDBField('TemplateName') . ' - ' . $template_type;
		}
		return $ret;
	}
	/**
	 * Applies design to given e-mail text
	 *
	 * @param string $text
	 * @param bool $is_html
	 * @return string
	 * @access protected
	 */
	protected function _applyMessageDesign($text, $is_html = true)
	{
		static $design_templates = Array();
		$design_key = 'L' . $this->params['language_id'] . ':' . ($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'), true);
			}
			$design_templates[$design_key] = $design_template;
		}
		return $this->_parseText(str_replace('$body', $text, $design_templates[$design_key]), $is_html);
	}
	/**
	 * Returns message body
	 *
	 * @param bool $is_html
	 * @return bool|string
	 * @access protected
	 */
	protected function _getMessageBody($is_html = false)
	{
		$message_body = $this->emailTemplate->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->emailTemplate->GetField('HtmlBody'), true);
		}
		if ( !trim($message_body) ) {
			return false;
		}
		if ( isset($this->params['use_custom_design']) && $this->params['use_custom_design'] ) {
			$message_body = $this->_parseText($message_body, $is_html);
		}
		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
	 * @param bool $is_html
	 * @return string
	 * @access protected
	 */
	protected function _parseText($text, $is_html = true)
	{
		$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($this->params);
		$text = $this->Application->Parser->Parse($this->_normalizeLineEndings($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($is_html ? $this->_removeTrailingLineEndings($text) : $text);
	}
	/**
	 * Substitutes replacement tags in given text
	 *
	 * @param string $text
	 * @return string
	 * @access protected
	 */
	protected function _substituteReplacementTags($text)
	{
		$default_replacement_tags = Array (
			' ' ' 'emailTemplate->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;
	}
	/**
	 * Convert Unix/Windows/Mac line ending into Unix line endings
	 *
	 * @param string $text
	 * @return string
	 * @access protected
	 */
	protected function _normalizeLineEndings($text)
	{
		return str_replace(Array ("\r\n", "\r"), "\n", $text);
	}
	/**
	 * Remove trailing line endings
	 *
	 * @param $text
	 * @return string
	 * @access protected
	 */
	protected function _removeTrailingLineEndings($text)
	{
		return preg_replace('/(\n|\r)+/', "\\1", $text);
	}
}