Index: branches/5.2.x/core/kernel/utility/email_send.php
===================================================================
--- branches/5.2.x/core/kernel/utility/email_send.php	(revision 16683)
+++ branches/5.2.x/core/kernel/utility/email_send.php	(revision 16684)
@@ -1,2173 +1,2181 @@
 <?php
 /**
 * @version	$Id$
 * @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 used to compose email message (using MIME standarts) and send it via mail function or via SMTP server
 	 *
 	 */
 	class kEmailSendingHelper extends kHelper {
 
 		/**
 		 * headers of main header part
 		 *
 		 * @var Array
 		 */
 		var $headers = Array ();
 
 		/**
 		 * Tells if all message parts were combined together
 		 *
 		 * @var int
 		 */
 		var $bodyPartNumber = false;
 
 		/**
 		 * Composed message parts
 		 *
 		 * @var Array
 		 */
 		var $parts = Array();
 
 		/**
 		 * Lines separator by MIME standart
 		 *
 		 * @var string
 		 */
 		var $line_break = "\n";
 
 		/**
 		 * Charset used for message composing
 		 *
 		 * @var string
 		 */
 		var $charset = 'utf-8';
 
 		/**
 		 * Name of mailer program (X-Mailer header)
 		 *
 		 * @var string
 		 */
 		var $mailerName = '';
 
 		/**
 		 * Options used for message content-type & structure guessing
 		 *
 		 * @var Array
 		 */
 		var $guessOptions = Array ();
 
 		/**
 		 * Send messages using selected method
 		 *
 		 * @var string
 		 */
 		var $sendMethod = 'Mail';
 
 		/**
 		 * Parameters used to initiate SMTP server connection
 		 *
 		 * @var Array
 		 */
 		var $smtpParams = Array ();
 
 	    /**
 	     * List of supported authentication methods, in preferential order.
 	     * @var array
 	     * @access public
 	     */
 	    var $smtpAuthMethods = Array('CRAM-MD5', 'LOGIN', 'PLAIN');
 
 	    /**
 	     * The socket resource being used to connect to the SMTP server.
 	     * @var kSocket
 	     * @access private
 	     */
 	    var $smtpSocket = null;
 
 	    /**
 	     * The most recent server response code.
 	     * @var int
 	     * @access private
 	     */
 	    var $smtpResponceCode = -1;
 
 	    /**
 	     * The most recent server response arguments.
 	     * @var array
 	     * @access private
 	     */
 	    var $smtpRespoceArguments = Array();
 
 	    /**
 	     * Stores detected features of the SMTP server.
 	     * @var array
 	     * @access private
 	     */
 	    var $smtpFeatures = Array();
 
 	    /**
 	     * Stores log data
 		 *
 	     * @var Array
 	     * @access protected
 	     */
 	    protected $_logData = Array ();
 
 		public function __construct()
 		{
 			parent::__construct();
 
 			// set default guess options
 			$this->guessOptions = Array (
 				'attachments'			=>	Array (),
 				'inline_attachments'	=>	Array (),
 				'text_part'				=>	false,
 				'html_part'				=>	false,
 			);
 
 			// read SMTP server connection params from config
 			$smtp_mapping = Array ('server' => 'Smtp_Server', 'port' => 'Smtp_Port');
 			if ($this->Application->ConfigValue('Smtp_Authenticate')) {
 				$smtp_mapping['username'] = 'Smtp_User';
 				$smtp_mapping['password'] = 'Smtp_Pass';
 			}
 
 			foreach ($smtp_mapping as $smtp_name => $config_name) {
 				$this->smtpParams[$smtp_name] = $this->Application->ConfigValue($config_name);
 			}
 			$this->smtpParams['use_auth'] = isset($this->smtpParams['username']) ? true : false;
 			$this->smtpParams['localhost'] = 'localhost'; // The value to give when sending EHLO or HELO.
 
 			$this->sendMethod = $this->smtpParams['server'] && $this->smtpParams['port'] ? 'SMTP' : 'Mail';
 
 			if ($this->sendMethod == 'SMTP') {
 				// create connection object if we will use SMTP
 				$this->smtpSocket = $this->Application->makeClass('Socket');
 			}
 
 			$this->SetCharset(null, true);
 		}
 
 		/**
 		 * Returns new message id header by sender's email address
 		 *
 		 * @param string $email_address email address
 		 * @return string
 		 */
 		function GenerateMessageID($email_address)
 		{
 			list ($micros, $seconds) = explode(' ', microtime());
 			list ($user, $domain) = explode('@', $email_address, 2);
 
 			$message_id = strftime('%Y%m%d%H%M%S', $seconds).substr($micros, 1, 5).'.'.preg_replace('/[^A-Za-z]+/', '-', $user).'@'.$domain;
 
 			$this->SetHeader('Message-ID', '<'.$message_id.'>');
 		}
 
 		/**
 		 * Returns extension of given filename
 		 *
 		 * @param string $filename
 		 * @return string
 		 */
 		function GetFilenameExtension($filename)
 		{
 			$last_dot = mb_strrpos($filename, '.');
 			return $last_dot !== false ? mb_substr($filename, $last_dot + 1) : '';
 		}
 
 		/**
 		 * Creates boundary for part by number (only if it's missing)
 		 *
 		 * @param int $part_number
 		 *
 		 */
 
 		function CreatePartBoundary($part_number)
 		{
 			$part =& $this->parts[$part_number];
 			if (!isset($part['BOUNDARY'])) {
 				$part['BOUNDARY'] = md5(uniqid($part_number.time()));
 
 			}
 		}
 
 		/**
 		 * Returns ready to use headers associative array of any message part by it's number
 		 *
 		 * @param int $part_number
 		 * @return Array
 		 */
 		function GetPartHeaders($part_number)
 		{
 			$part =& $this->parts[$part_number];
 
 			if (!isset($part['Content-Type'])) {
 				return $this->SetError('MISSING_CONTENT_TYPE');
 			}
 
 			$full_type = strtolower($part['Content-Type']);
 			list ($type, $sub_type) = explode('/', $full_type);
 
 			$headers['Content-Type'] = $full_type;
 			switch ($type) {
 				case 'text':
 				case 'image':
 				case 'audio':
 				case 'video':
 				case 'application':
 				case 'message':
 					// 1. update content-type header
 					if (isset($part['CHARSET'])) {
 						$headers['Content-Type'] .= '; charset='.$part['CHARSET'];
 					}
 					if (isset($part['NAME'])) {
 						$headers['Content-Type'] .= '; name="'.$part['NAME'].'"';
 					}
 
 					// 2. set content-transfer-encoding header
 					if (isset($part['Content-Transfer-Encoding'])) {
 						$headers['Content-Transfer-Encoding'] = $part['Content-Transfer-Encoding'];
 					}
 
 					// 3. set content-disposition header
 					if (isset($part['DISPOSITION']) && $part['DISPOSITION']) {
 						$headers['Content-Disposition'] = $part['DISPOSITION'];
 						if (isset($part['NAME'])) {
 							$headers['Content-Disposition'] .= '; filename="'.$part['NAME'].'"';
 						}
 					}
 					break;
 
 				case 'multipart':
 					switch ($sub_type) {
 						case 'alternative':
 						case 'related':
 						case 'mixed':
 						case 'parallel':
 							$this->CreatePartBoundary($part_number);
 							$headers['Content-Type'] .= '; boundary="'.$part['BOUNDARY'].'"';
 							break;
 
 						default:
 							return $this->SetError('INVALID_MULTIPART_SUBTYPE', Array($sub_type));
 					}
 					break;
 
 				default:
 					return $this->SetError('INVALID_CONTENT_TYPE', Array($full_type));
 			}
 
 			// set content-id if any
 			if (isset($part['Content-ID'])) {
 				$headers['Content-ID'] = '<'.$part['Content-ID'].'>';
 			}
 
 			return $headers;
 		}
 
 		function GetPartBody($part_number)
 		{
 			$part =& $this->parts[$part_number];
 
 			if (!isset($part['Content-Type'])) {
 				return $this->SetError('MISSING_CONTENT_TYPE');
 			}
 
 			$full_type = strtolower($part['Content-Type']);
 			list ($type, $sub_type) = explode('/', $full_type);
 
 			$body = '';
 			switch ($type) {
 				// compose text/binary content
 				case 'text':
 				case 'image':
 				case 'audio':
 				case 'video':
 				case 'application':
 				case 'message':
 					// 1. get content of part
 					if (isset($part['FILENAME'])) {
 						// content provided via absolute path to content containing file
 						$filename = $part['FILENAME'];
 						$file_size = filesize($filename);
 
 						$body = file_get_contents($filename);
 						if ($body === false) {
 							return $this->SetError('FILE_PART_OPEN_ERROR', Array($filename));
 						}
 
 						$actual_size = strlen($body);
 						if (($file_size === false || $actual_size > $file_size) && get_magic_quotes_runtime()) {
 							$body = stripslashes($body);
 						}
 
 						if ($file_size !== false && $actual_size != $file_size) {
 							return $this->SetError('FILE_PART_DATA_ERROR', Array($filename));
 						}
 					}
 					else {
 						// content provided directly as one of part keys
 						if (!isset($part['DATA'])) {
 							return $this->SetError('FILE_PART_DATA_MISSING');
 						}
 						$body =& $part['DATA'];
 					}
 
 					// 2. get part transfer encoding
 					$encoding = isset($part['Content-Transfer-Encoding']) ? strtolower($part['Content-Transfer-Encoding']) : '';
 					if (!in_array($encoding, Array ('', 'base64', 'quoted-printable', '7bit'))) {
 						return $this->SetError('INVALID_ENCODING', Array($encoding));
 					}
 
 					if ($encoding == 'base64') {
 						// split base64 encoded text by 76 symbols at line (MIME requirement)
 						$body = chunk_split( base64_encode($body) );
 					}
 					break;
 
 				case 'multipart':
 					// compose multipart message
 					switch ($sub_type) {
 						case 'alternative':
 						case 'related':
 						case 'mixed':
 						case 'parallel':
 							$this->CreatePartBoundary($part_number);
 							$boundary = $this->line_break.'--'.$part['BOUNDARY'];
 
 							foreach ($part['PARTS'] as $multipart_number) {
 								$body .= $boundary.$this->line_break;
 								$part_headers = $this->GetPartHeaders($multipart_number);
 								if ($part_headers === false) {
 									// some of sub-part headers were invalid
 									return false;
 								}
 
 								foreach ($part_headers as $header_name => $header_value) {
 									$body .= $header_name.': '.$header_value.$this->line_break;
 								}
 
 								$part_body = $this->GetPartBody($multipart_number);
 								if ($part_body === false) {
 									// part body was invalid
 									return false;
 								}
 
 								$body .= $this->line_break.$part_body;
 							}
 							$body .= $boundary.'--'.$this->line_break;
 							break;
 
 						default:
 							return $this->SetError('INVALID_MULTIPART_SUBTYPE', Array($sub_type));
 					}
 					break;
 				default:
 					return $this->SetError('INVALID_CONTENT_TYPE', Array($full_type));
 			}
 
 			return $body;
 		}
 
 		/**
 		 * Applies quoted-printable encoding to specified text
 		 *
 		 * @param string $text
 		 * @param string $header_charset
 		 * @param int $break_lines
 		 * @return unknown
 		 */
 		function QuotedPrintableEncode($text, $header_charset = '', $break_lines = 1)
 		{
 			$ln = strlen($text);
 			$h = strlen($header_charset) > 0;
 			if ($h) {
 				$s = Array (
 					'=' => 1,
 					'?' => 1,
 					'_' => 1,
 					'(' => 1,
 					')' => 1,
 					'<' => 1,
 					'>' => 1,
 					'@' => 1,
 					',' => 1,
 					';' => 1,
 					'"' => 1,
 					'\\' => 1,
 	/*
 					'/' => 1,
 					'[' => 1,
 					']' => 1,
 					':' => 1,
 					'.' => 1,
 	*/
 				);
 
 				$b = $space = $break_lines = 0;
 				for ($i = 0; $i < $ln; $i++) {
 					if (isset($s[$text[$i]])) {
 						$b = 1;
 						break;
 					}
 
 					switch ($o = ord($text[$i])) {
 						case 9:
 						case 32:
 							$space = $i + 1;
 							$b = 1;
 							break 2;
 						case 10:
 						case 13:
 							break 2;
 						default:
 						if ($o < 32 || $o > 127) {
 							$b = 1;
 							break 2;
 						}
 					}
 				}
 
 				if($i == $ln) {
 					return $text;
 				}
 
 				if ($space > 0) {
 					return substr($text, 0, $space).($space < $ln ? $this->QuotedPrintableEncode(substr($text, $space), $header_charset, 0) : '');
 				}
 			}
 
 			for ($w = $e = '', $n = 0, $l = 0, $i = 0; $i < $ln; $i++) {
 				$c = $text[$i];
 				$o = ord($c);
 				$en = 0;
 				switch ($o) {
 					case 9:
 					case 32:
 						if (!$h) {
 							$w = $c;
 							$c = '';
 						}
 						else {
 							if ($b) {
 								if ($o == 32) {
 									$c = '_';
 								}
 								else {
 									$en = 1;
 								}
 							}
 						}
 						break;
 					case 10:
 					case 13:
 						if (strlen($w)) {
 							if ($break_lines && $l + 3 > 75) {
 								$e .= '='.$this->line_break;
 								$l = 0;
 							}
 
 							$e .= sprintf('=%02X', ord($w));
 							$l += 3;
 							$w = '';
 						}
 
 						$e .= $c;
 						if ($h) {
 							$e .= "\t";
 						}
 						$l = 0;
 						continue 2;
 					case 46:
 					case 70:
 					case 102:
 						$en = (!$h && ($l == 0 || $l + 1 > 75));
 						break;
 					default:
 						if ($o > 127 || $o < 32 || !strcmp($c, '=')) {
 							$en = 1;
 						}
 						elseif ($h && isset($s[$c])) {
 							$en = 1;
 						}
 						break;
 				}
 
 				if (strlen($w)) {
 					if ($break_lines && $l + 1 > 75) {
 						$e .= '='.$this->line_break;
 						$l = 0;
 					}
 					$e .= $w;
 					$l++;
 					$w = '';
 				}
 
 				if (strlen($c)) {
 					if ($en) {
 						$c = sprintf('=%02X', $o);
 						$el = 3;
 						$n = 1;
 						$b = 1;
 					}
 					else {
 						$el = 1;
 					}
 					if ($break_lines && $l + $el > 75) {
 						$e .= '='.$this->line_break;
 						$l = 0;
 					}
 					$e .= $c;
 					$l += $el;
 				}
 			}
 			if (strlen($w)) {
 				if ($break_lines && $l + 3 > 75) {
 					$e .= '='.$this->line_break;
 				}
 				$e .= sprintf('=%02X', ord($w));
 			}
 
 			return $h && $n ? '=?'.$header_charset.'?q?'.$e.'?=' : $e;
 		}
 
 		/**
 		 * Sets message header + encodes is by quoted-printable using charset specified
 		 *
 		 * @param string $name
 		 * @param string $value
 		 * @param string $encoding_charset
 		 */
 		function SetHeader($name, $value, $encoding_charset = '')
 		{
 			if ($encoding_charset) {
 				// actually for headers base64 method may give shorter result
 				$value = $this->QuotedPrintableEncode($value, $encoding_charset);
 			}
 
 			$this->headers[$name] = $value;
 		}
 
 		/**
 		 * Sets header + automatically encodes it using default charset
 		 *
 		 * @param string $name
 		 * @param string $value
 		 */
 		function SetEncodedHeader($name, $value)
 		{
 			$this->SetHeader($name, $value, $this->charset);
 		}
 
 		/**
 		 * Sets header which value is email and username +autoencode
 		 *
 		 * @param string $header
 		 * @param string $address
 		 * @param string $name
 		 */
 		function SetEncodedEmailHeader($header, $address, $name)
 		{
 			$this->SetHeader($header, $this->QuotedPrintableEncode($name, $this->charset) . ' <' . $address . '>');
 		}
 
 		function SetMultipleEncodedEmailHeader($header, $addresses)
 		{
 			$value = '';
 			foreach ($addresses as $name => $address) {
 				$value .= $this->QuotedPrintableEncode($name, $this->charset) . ' <' . $address . '>, ';
 			}
 
 			$this->SetHeader($header, substr($value, 0, -2));
 		}
 
 		/**
 		 * Adds new part to message and returns it's number
 		 *
 		 * @param Array $part_definition
 		 * @param int|bool $part_number number of new part
 		 * @return int
 		 */
 		function AddPart(&$part_definition, $part_number = false)
 		{
 			$part_number = $part_number !== false ? $part_number : count($this->parts);
 			$this->parts[$part_number] =& $part_definition;
 			return $part_number;
 		}
 
 		/**
 		 * Detects if such part already was added.
 		 *
 		 * @param array $part_definition Part definition.
 		 *
 		 * @return boolean
 		 */
 		protected function HasPart(array $part_definition)
 		{
 			foreach ( $this->parts as $part ) {
 				if ( $part === $part_definition ) {
 					return true;
 				}
 			}
 
 			return false;
 		}
 
 		/**
 		 * Returns text version of HTML document
 		 *
 		 * @param string $html
 		 * @param bool $keep_inp_tags
 		 * @return string
 		 */
 		function ConvertToText($html, $keep_inp_tags = false)
 		{
 			if ( $keep_inp_tags && preg_match_all('/(<[\\/]?)inp2:([^>]*?)([\\/]?>)/s', $html, $regs) ) {
 				$found_tags = Array ();
 
 				foreach ($regs[0] as $index => $tag) {
 					$tag_placeholder = '%' . md5($index . ':' . $tag) . '%';
 					$found_tags[$tag_placeholder] = $tag;
 
 					// we can have duplicate tags -> replace only 1st occurrence (str_replace can't do that)
 					$html = preg_replace('/' . preg_quote($tag, '/') . '/', $tag_placeholder, $html, 1);
 				}
 
 				$html = $this->_convertToText($html);
 
 				foreach ($found_tags as $tag_placeholder => $tag) {
 					$html = str_replace($tag_placeholder, $tag, $html);
 				}
 
 				return $html;
 			}
 
 			return $this->_convertToText($html);
 		}
 
 		/**
 		 * Returns text version of HTML document
 		 *
 		 * @param string $html
 		 * @return string
 		 */
 		protected function _convertToText($html)
 		{
 			$search = Array (
 				"'(<\/td>.*)[\r\n]+(.*<td)'i",//formating text in tables
 				"'(<br[ ]?[\/]?>[\r\n]{0,2})|(<\/p>)|(<\/div>)|(<\/tr>)'i",
 				"'<head>(.*?)</head>'si",
 				"'<style(.*?)</style>'si",
 				"'<title>(.*?)</title>'si",
 				"'<script(.*?)</script>'si",
 //				"'^[\s\n\r\t]+'", //strip all spacers & newlines in the begin of document
 //				"'[\s\n\r\t]+$'", //strip all spacers & newlines in the end of document
 				"'&(quot|#34);'i",
 				"'&(amp|#38);'i",
 				"'&(lt|#60);'i",
 				"'&(gt|#62);'i",
 				"'&(nbsp|#160);'i",
 				"'&(iexcl|#161);'i",
 				"'&(cent|#162);'i",
 				"'&(pound|#163);'i",
 				"'&(copy|#169);'i",
-				"'&#(\d+);'e"
 			);
 
 			$replace = Array (
 				"\\1\t\\2",
 				"\n",
 				"",
 				"",
 				"",
 				"",
 //				"",
 //				"",
 				"\"",
 				"&",
 				"<",
 				">",
 				" ",
 				$this->_safeCharEncode(161),
 				$this->_safeCharEncode(162),
 				$this->_safeCharEncode(163),
 				$this->_safeCharEncode(169),
-				"\$this->_safeCharEncode(\\1)"
 			);
 
-			return strip_tags( preg_replace ($search, $replace, $html) );
+			$html = preg_replace($search, $replace, $html);
+			$that = $this;
+			$html = preg_replace_callback(
+				"'&#(\d+);'",
+				function ($matches) use ($that) {
+					return $that->_safeCharEncode($matches[1]);
+				},
+				$html
+			);
+
+			return strip_tags($html);
 		}
 
 		/**
 		 * Returns symbols, that corresponds given ASCII code and also encodes it into proper charset
 		 *
 		 * @param int $ascii
 		 * @return string
 		 * @access protected
 		 */
-		protected function _safeCharEncode($ascii)
+		public function _safeCharEncode($ascii)
 		{
 			return mb_convert_encoding(chr($ascii), $this->charset, 'ISO-8859-1');
 		}
 
 		/**
 		 * Add text OR html part to message (optionally encoded)
 		 *
 		 * @param string $text part's text
 		 * @param bool $is_html this html part or not
 		 * @param bool $encode encode message using quoted-printable encoding
 		 *
 		 * @return int number of created part
 		 */
 		function CreateTextHtmlPart($text, $is_html = false, $encode = true)
 		{
 			if ($is_html) {
 				// if adding HTML part, then create plain-text part too
 				$this->CreateTextHtmlPart($this->ConvertToText($text));
 			}
 
 			// in case if text is from $_REQUEST, then line endings are "\r\n", but we need "\n" here
 
 			$text = str_replace("\r\n", "\n", $text); // possible case
 			$text = str_replace("\r", "\n", $text); // impossible case, but just in case replace this too
 
 			$definition = Array (
 				'Content-Type'	=>	$is_html ? 'text/html' : 'text/plain',
 				'CHARSET'		=>	$this->charset,
 				'DATA'			=>	$encode ? $this->QuotedPrintableEncode($text) : $text,
 			);
 
 			if ($encode) {
 				$definition['Content-Transfer-Encoding'] = 'quoted-printable';
 			}
 
 			$guess_name = $is_html ? 'html_part' : 'text_part';
 			$part_number = $this->guessOptions[$guess_name] !== false ? $this->guessOptions[$guess_name] : false;
 
 			$part_number = $this->AddPart($definition, $part_number);
 			$this->guessOptions[$guess_name] = $part_number;
 
 			return $part_number;
 		}
 
 		/**
 		 * Adds attachment part to message
 		 *
 		 * @param string $file name of the file with attachment body
 		 * @param string $attach_name name for attachment (name of file is used, when not specified)
 		 * @param string $content_type content type for attachment
 		 * @param string $content body of file to be attached
 		 * @param bool $inline is attachment inline or not
 		 *
 		 * @return int number of created part
 		 */
 		function AddAttachment($file = '', $attach_name = '', $content_type = '', $content = '', $inline = false)
 		{
 			$definition = Array (
 				'Disposition'	=>	$inline ? 'inline' : 'attachment',
 				'Content-Type'	=>	$content_type ? $content_type : 'automatic/name',
 			);
 
 			if ($file) {
 				// filename of attachment given
 				$definition['FileName'] = $file;
 			}
 
 			if ($attach_name) {
 				// name of attachment given
 				$definition['Name'] = $attach_name;
 			}
 
 			if ($content) {
 				// attachment data is given
 				$definition['Data'] = $content;
 			}
 
 			$definition =& $this->GetFileDefinition($definition);
 
 			if ( $this->HasPart($definition) ) {
 				return false;
 			}
 
 			$part_number = $this->AddPart($definition);
 
 			if ($inline) {
 				// it's inline attachment and needs content-id to be addressed by in message
 				$this->parts[$part_number]['Content-ID'] = md5(uniqid($part_number.time())).'.'.$this->GetFilenameExtension($attach_name ? $attach_name : $file);
 			}
 
 			$this->guessOptions[$inline ? 'inline_attachments' : 'attachments'][] = $part_number;
 
 			return $part_number;
 		}
 
 		/**
 		 * Adds another MIME message as attachment to message being composed
 		 *
 		 * @param string $file name of the file with attachment body
 		 * @param string $attach_name name for attachment (name of file is used, when not specified)
 		 * @param string $content body of file to be attached
 		 *
 		 * @return int number of created part
 		 */
 		function AddMessageAttachment($file = '', $attach_name = '', $content = '')
 		{
 			$part_number = $this->AddAttachment($file, $attach_name, 'message/rfc822', $content, true);
 			unset($this->parts[$part_number]['Content-ID']); // messages don't have content-id, but have inline disposition
 			return $part_number;
 		}
 
 		/**
 		 * Creates multipart of specified type and returns it's number
 		 *
 		 * @param Array $part_numbers
 		 * @param string $multipart_type = {alternative,related,mixed,paralell}
 		 * @return int
 		 */
 		function CreateMultipart($part_numbers, $multipart_type)
 		{
 			$types = Array ('alternative', 'related' , 'mixed', 'paralell');
 			if (!in_array($multipart_type, $types)) {
 				return $this->SetError('INVALID_MULTIPART_SUBTYPE', Array($multipart_type));
 			}
 
 			$definition = Array (
 				'Content-Type'	=>	'multipart/'.$multipart_type,
 				'PARTS'			=>	$part_numbers,
 			);
 
 			return $this->AddPart($definition);
 		}
 
 		/**
 		 * Creates missing content-id header for inline attachments
 		 *
 		 * @param int $part_number
 		 */
 		function CreateContentID($part_number)
 		{
 			$part =& $this->parts[$part_number];
 			if (!isset($part['Content-ID']) && $part['DISPOSITION'] == 'inline') {
 				$part['Content-ID'] = md5(uniqid($part_number.time())).'.'.$this->GetFilenameExtension($part['NAME']);
 			}
 		}
 
 		/**
 		 * Returns attachment part based on file used in attachment
 		 *
 		 * @param Array $file
 		 * @return Array
 		 */
 		function &GetFileDefinition ($file)
 		{
 			$name = '';
 			if (isset($file['Name'])) {
 				// if name is given directly, then use it
 				$name = $file['Name'];
 			}
 			else {
 				// auto-guess attachment name based on source filename
 				$name = isset($file['FileName']) ? basename($file['FileName']) : '';
 			}
 
 			if (!$name || (!isset($file['FileName']) && !isset($file['Data']))) {
 				// filename not specified || no filename + no direct file content
 				return $this->SetError('MISSING_FILE_DATA');
 			}
 
 			$encoding = 'base64';
 			if (isset($file['Content-Type'])) {
 				$content_type = $file['Content-Type'];
 				list ($type, $sub_type) = explode('/', $content_type);
 
 				switch ($type) {
 					case 'text':
 					case 'image':
 					case 'audio':
 					case 'video':
 					case 'application':
 						break;
 
 					case 'message':
 						$encoding = '7bit';
 						break;
 
 					case 'automatic':
 						if (!$name) {
 							return $this->SetError('MISSING_FILE_NAME');
 						}
 						$this->guessContentType($name, $content_type, $encoding);
 						break;
 
 					default:
 						return $this->SetError('INVALID_CONTENT_TYPE', Array($content_type));
 				}
 			}
 			else {
 				// encoding not passed in file part, then assume, that it's binary
 				$content_type = 'application/octet-stream';
 			}
 
 			$definition = Array (
 				'Content-Type'				=>	$content_type,
 				'Content-Transfer-Encoding'	=>	$encoding,
 				'NAME'						=>	$name, // attachment name
 			);
 
 			if (isset($file['Disposition'])) {
 				$disposition = strtolower($file['Disposition']);
 				if ($disposition == 'inline' || $disposition == 'attachment') {
 					// valid disposition header value
 					$definition['DISPOSITION'] = $file['Disposition'];
 				}
 				else {
 					return $this->SetError('INVALID_DISPOSITION', Array($file['Disposition']));
 				}
 			}
 
 			if (isset($file['FileName'])) {
 				$definition['FILENAME'] = $file['FileName'];
 			}
 			elseif (isset($file['Data'])) {
 				$definition['DATA'] =& $file['Data'];
 			}
 
 			return $definition;
 		}
 
 		/**
 		 * Returns content-type based on filename extension
 		 *
 		 * @param string $filename
 		 * @param string $content_type
 		 * @param string $encoding
 		 *
 		 * @todo Regular expression used is not completely finished, that's why if extension used for
 		 * comparing in some other extension (from list) part, that partial match extension will be returned.
 		 * Because of two extension that begins with same 2 letters always belong to same content type
 		 * this unfinished regular expression still gives correct result in any case.
 		 */
 		function guessContentType($filename, &$content_type, &$encoding)
 		{
 			$content_type = kUtil::mimeContentTypeByExtension($filename);
 
 			if ( mb_strtolower($this->GetFilenameExtension($filename)) == 'eml' ) {
 				$encoding = '7bit';
 			}
 		}
 
 		/**
 		 * Using guess options combines all added parts together and returns combined part number
 		 *
 		 * @return int
 		 */
 		function PrepareMessageBody()
 		{
 			if ($this->bodyPartNumber === false) {
 				$part_number = false; // number of generated body part
 
 				// 1. set text content of message
 				if ($this->guessOptions['text_part'] !== false && $this->guessOptions['html_part'] !== false) {
 					// text & html parts present -> compose into alternative part
 					$parts = Array (
 						$this->guessOptions['text_part'],
 						$this->guessOptions['html_part'],
 					);
 					$part_number = $this->CreateMultipart($parts, 'alternative');
 				}
 				elseif ($this->guessOptions['text_part'] !== false) {
 					// only text part is defined, then leave as is
 					$part_number = $this->guessOptions['text_part'];
 				}
 
 				if ($part_number === false) {
 					return $this->SetError('MESSAGE_TEXT_MISSING');
 				}
 
 				// 2. if inline attachments found, then create related multipart from text & inline attachments
 				if ($this->guessOptions['inline_attachments']) {
 					$parts = array_merge(Array($part_number), $this->guessOptions['inline_attachments']);
 					$part_number = $this->CreateMultipart($parts, 'related');
 				}
 
 				// 3. if normal attachments found, then create mixed multipart from text & attachments
 				if ($this->guessOptions['attachments']) {
 					$parts = array_merge(Array($part_number), $this->guessOptions['attachments']);
 					$part_number = $this->CreateMultipart($parts, 'mixed');
 				}
 
 				$this->bodyPartNumber = $part_number;
 			}
 
 			return $this->bodyPartNumber;
 		}
 
 		/**
 		 * Returns message headers and body part (by reference in parameters)
 		 *
 		 * @param Array $message_headers
 		 * @param string $message_body
 		 * @return bool
 		 */
 		function GetHeadersAndBody(&$message_headers, &$message_body)
 		{
 			$part_number = $this->PrepareMessageBody();
 			if ($part_number === false) {
 				return $this->SetError('MESSAGE_COMPOSE_ERROR');
 			}
 
 			$message_headers = $this->GetPartHeaders($part_number);
 
 			// join message headers and body headers
 			$message_headers = array_merge($this->headers, $message_headers);
 
 			$message_headers['MIME-Version'] = '1.0';
 			if ($this->mailerName) {
 				$message_headers['X-Mailer'] = $this->mailerName;
 			}
 
 			$this->GenerateMessageID($message_headers['From']);
 			$valid_headers = $this->ValidateHeaders($message_headers);
 			if ($valid_headers) {
 				// set missing headers from existing
 				$from_headers = Array ('Reply-To', 'Errors-To');
 				foreach ($from_headers as $header_name) {
 					if (!isset($message_headers[$header_name])) {
 						$message_headers[$header_name] = $message_headers['From'];
 					}
 				}
 
 				$message_body = $this->GetPartBody($part_number);
 				return true;
 			}
 
 			return false;
 		}
 
 		/**
 		 * Checks that all required headers are set and not empty
 		 *
 		 * @param Array $message_headers
 		 * @return bool
 		 */
 		function ValidateHeaders($message_headers)
 		{
 			$from = isset($message_headers['From']) ? $message_headers['From'] : '';
 			if (!$from) {
 				return $this->SetError('HEADER_MISSING', Array('From'));
 			}
 
 			if (!isset($message_headers['To'])) {
 				return $this->SetError('HEADER_MISSING', Array('To'));
 			}
 
 			if (!isset($message_headers['Subject'])) {
 				return $this->SetError('HEADER_MISSING', Array('Subject'));
 			}
 
 			return true;
 		}
 
 		/**
 		 * Returns full message source (headers + body) for sending to SMTP server
 		 *
 		 * @return string
 		 */
 		function GetMessage()
 		{
 			$composed = $this->GetHeadersAndBody($message_headers, $message_body);
 			if ($composed) {
 				// add headers to resulting message
 				$message = '';
 				foreach ($message_headers as $header_name => $header_value) {
 					$message .= $header_name.': '.$header_value.$this->line_break;
 				}
 
 				// add message body
 				$message .= $this->line_break.$message_body;
 
 				return $message;
 			}
 
 			return false;
 		}
 
 		/**
 		 * Sets just happened error code
 		 *
 		 * @param string $code
 		 * @param Array $params additional error params
 		 * @param bool $fatal
 		 * @return bool
 		 */
 		function SetError($code, $params = null, $fatal = true)
 		{
 			$error_msgs = Array (
 				'MAIL_NOT_FOUND'			=>	'the mail() function is not available in this PHP installation',
 
 				'MISSING_CONTENT_TYPE'		=>	'it was added a part without Content-Type: defined',
 				'INVALID_CONTENT_TYPE'		=>	'Content-Type: %s not yet supported',
 				'INVALID_MULTIPART_SUBTYPE'	=>	'multipart Content-Type sub_type %s not yet supported',
 				'FILE_PART_OPEN_ERROR'		=>	'could not open part file %s',
 				'FILE_PART_DATA_ERROR'		=>	'the length of the file that was read does not match the size of the part file %s due to possible data corruption',
 				'FILE_PART_DATA_MISSING'	=>	'it was added a part without a body PART',
 				'INVALID_ENCODING'			=>	'%s is not yet a supported encoding type',
 
 				'MISSING_FILE_DATA'			=>	'file part data is missing',
 				'MISSING_FILE_NAME'			=>	'it is not possible to determine content type from the name',
 				'INVALID_DISPOSITION'		=>	'%s is not a supported message part content disposition',
 
 				'MESSAGE_TEXT_MISSING'		=>	'text part of message was not defined',
 				'MESSAGE_COMPOSE_ERROR'		=>	'unknown message composing error',
 
 				'HEADER_MISSING'			=>	'header %s is required',
 
 				// SMTP errors
 				'INVALID_COMMAND'				=>	'Commands cannot contain newlines',
 				'CONNECTION_TERMINATED'			=>	'Connection was unexpectedly closed',
 				'HELO_ERROR'					=>	'HELO was not accepted: %s',
 				'AUTH_METHOD_NOT_SUPPORTED'		=>	'%s is not a supported authentication method',
 				'AUTH_METHOD_NOT_IMPLEMENTED'	=>	'%s is not a implemented authentication method',
 			);
 
 			if (!is_array($params)) {
 				$params = Array ();
 			}
 
 			$error_msg = 'mail error: ' . vsprintf($error_msgs[$code], $params);
 
 			if ($fatal) {
 				throw new Exception($error_msg);
 			}
 			else {
 				if ( $this->Application->isDebugMode() ) {
 					$this->Application->Debugger->appendTrace();
 				}
 
 				trigger_error($error_msg, E_USER_WARNING);
 			}
 
 			return false;
 		}
 
 		/**
 		 * Simple method of message sending
 		 *
 		 * @param string $from_email
 		 * @param string $to_email
 		 * @param string $subject
 		 * @param string $from_name
 		 * @param string $to_name
 		 */
 		function Send($from_email, $to_email, $subject, $from_name = '', $to_name = '')
 		{
 			$this->SetSubject($subject);
 			$this->SetFrom($from_email, trim($from_name) ? trim($from_name) : $from_email);
 
 			if (!isset($this->headers['Return-Path'])) {
 				$this->SetReturnPath($from_email);
 			}
 
 			$this->SetTo($to_email, $to_name ? $to_name : $to_email);
 
 			return $this->Deliver();
 		}
 
 		/**
 		 * Prepares class for sending another message
 		 *
 		 */
 		function Clear()
 		{
 			$this->headers = Array ();
 			$this->bodyPartNumber = false;
 			$this->parts = Array ();
 			$this->guessOptions = Array (
 				'attachments'			=>	Array (),
 				'inline_attachments'	=>	Array (),
 				'text_part'				=>	false,
 				'html_part'				=>	false,
 			);
 
 			$this->SetCharset(null, true);
 			$this->_logData = Array ();
 		}
 
 		/**
 		 * Sends message via php mail function
 		 *
 		 * @param Array $message_headers
 		 * @param string $body
 		 *
 		 * @return bool
 		 */
 		function SendMail($message_headers, &$body)
 		{
 			if (!function_exists('mail')) {
 				return $this->SetError('MAIL_NOT_FOUND');
 			}
 
 			$to = $message_headers['To'];
 			$subject = $message_headers['Subject'];
 			$return_path = $message_headers['Return-Path'];
 			unset($message_headers['To'], $message_headers['Subject']);
 
 			$headers = '';
 			$header_separator = $this->Application->ConfigValue('MailFunctionHeaderSeparator') == 1 ? "\n" : "\r\n";
 			foreach ($message_headers as $header_name => $header_value) {
 				$headers .= $header_name.': '.$header_value.$header_separator;
 			}
 
 			if ($return_path) {
 				if (kUtil::constOn('SAFE_MODE') || (defined('PHP_OS') && substr(PHP_OS, 0, 3) == 'WIN')) {
 					// safe mode restriction OR is windows
 					$return_path = '';
 				}
 			}
 
 			return mail($to, $subject, $body, $headers, $return_path ? '-f'.$return_path : null);
 		}
 
 		/**
 		 * Sends message via SMTP server
 		 *
 		 * @param Array $message_headers
 		 * @param string $message_body
 		 * @return bool
 		 */
 		function SendSMTP($message_headers, &$message_body)
 		{
 			if (!$this->SmtpConnect()) {
 				return false;
 			}
 
 			$from = $this->ExtractRecipientEmail($message_headers['From']);
 			if (!$this->SmtpSetFrom($from)) {
 				return false;
 			}
 
 			$recipients = '';
 			$recipient_headers = Array ('To', 'Cc', 'Bcc');
 			foreach ($recipient_headers as $recipient_header) {
 				if (isset($message_headers[$recipient_header])) {
 					$recipients .= ' '.$message_headers[$recipient_header];
 				}
 			}
 
 			$recipients_accepted = 0;
 			$recipients = $this->ExtractRecipientEmail($recipients, true);
 			foreach ($recipients as $recipient) {
 				if ($this->SmtpAddTo($recipient)) {
 					$recipients_accepted++;
 				}
 			}
 
 			if ($recipients_accepted == 0) {
 				// none of recipients were accepted
 				return false;
 			}
 
 			$headers = '';
 			foreach ($message_headers as $header_name => $header_value) {
 				$headers .= $header_name.': '.$header_value.$this->line_break;
 			}
 
 			if (!$this->SmtpSendMessage($headers . "\r\n" . $message_body)) {
 				return false;
 			}
 
 			$this->SmtpDisconnect();
 
 			return true;
 		}
 
 		/**
 	     * Send a command to the server with an optional string of
 	     * arguments.  A carriage return / linefeed (CRLF) sequence will
 	     * be appended to each command string before it is sent to the
 	     * SMTP server.
 	     *
 	     * @param string $command The SMTP command to send to the server.
 	     * @param string $args A string of optional arguments to append to the command.
 	     *
 	     * @return bool
 	     *
 	     */
 	    function SmtpSendCommand($command, $args = '')
 	    {
 	        if (!empty($args)) {
 	            $command .= ' ' . $args;
 	        }
 
 	        if (strcspn($command, "\r\n") !== strlen($command)) {
 	            return $this->SetError('INVALID_COMMAND');
 	        }
 
 	        return $this->smtpSocket->write($command . "\r\n") === false ? false : true;
 	    }
 
 	    /**
 	     * Read a reply from the SMTP server. The reply consists of a response code and a response message.
 	     *
 	     * @param mixed $valid The set of valid response codes. These may be specified as an array of integer values or as a single integer value.
 	     *
 	     * @return  bool
 	     *
 	     */
 	    function SmtpParseResponse($valid)
 	    {
 	        $this->smtpResponceCode = -1;
 	        $this->smtpRespoceArguments = array();
 
 	        while ($line = $this->smtpSocket->readLine()) {
 	            // If we receive an empty line, the connection has been closed.
 	            if (empty($line)) {
 	                $this->SmtpDisconnect();
 	                return $this->SetError('CONNECTION_TERMINATED', null, false);
 	            }
 
 	            // Read the code and store the rest in the arguments array.
 	            $code = substr($line, 0, 3);
 	            $this->smtpRespoceArguments[] = trim(substr($line, 4));
 
 	            // Check the syntax of the response code.
 	            if (is_numeric($code)) {
 	                $this->smtpResponceCode = (int)$code;
 	            } else {
 	                $this->smtpResponceCode = -1;
 	                break;
 	            }
 
 	            // If this is not a multiline response, we're done.
 	            if (substr($line, 3, 1) != '-') {
 	                break;
 	            }
 	        }
 
 	        // Compare the server's response code with the valid code.
 	        if (is_int($valid) && ($this->smtpResponceCode === $valid)) {
 	            return true;
 	        }
 
 	        // If we were given an array of valid response codes, check each one.
 	        if (is_array($valid)) {
 	            foreach ($valid as $valid_code) {
 	                if ($this->smtpResponceCode === $valid_code) {
 	                    return true;
 	                }
 	            }
 	        }
 
 	        return false;
 	    }
 
 	    /**
 	     * Attempt to connect to the SMTP server.
 	     *
 	     * @param int $timeout The timeout value (in seconds) for the socket connection.
 	     * @param bool $persistent Should a persistent socket connection be used ?
 	     *
 	     * @return bool
 	     *
 	     */
 	    function SmtpConnect($timeout = null, $persistent = false)
 	    {
 	        $result = $this->smtpSocket->connect($this->smtpParams['server'], $this->smtpParams['port'], $persistent, $timeout);
 	        if (!$result) {
 	        	return false;
 	        }
 
 	        if ($this->SmtpParseResponse(220) === false) {
 	        	return false;
 	        }
 	        elseif ($this->SmtpNegotiate() === false) {
 	        	return false;
 	        }
 
 	        if ($this->smtpParams['use_auth']) {
 	        	$result = $this->SmtpAuthentificate($this->smtpParams['username'], $this->smtpParams['password']);
 	        	if (!$result) {
 	        		// authentification failed
 	        		return false;
 	        	}
 	        }
 
 	        return true;
 	    }
 
 	    /**
 	     * Attempt to disconnect from the SMTP server.
 	     *
 	     * @return bool
 	     */
 	    function SmtpDisconnect()
 	    {
 	        if ($this->SmtpSendCommand('QUIT') === false) {
 	        	return false;
 	        }
 	        elseif ($this->SmtpParseResponse(221) === false) {
 	        	return false;
 	        }
 
 	        return $this->smtpSocket->disconnect();
 	    }
 
 	    /**
 	     * Attempt to send the EHLO command and obtain a list of ESMTP
 	     * extensions available, and failing that just send HELO.
 	     *
 	     * @return bool
 	     */
 	    function SmtpNegotiate()
 	    {
 	        if (!$this->SmtpSendCommand('EHLO', $this->smtpParams['localhost'])) {
 	        	return false;
 	        }
 
 	        if (!$this->SmtpParseResponse(250)) {
 	            // If we receive a 503 response, we're already authenticated.
 	            if ($this->smtpResponceCode === 503) {
 	                return true;
 	            }
 
 	            // If the EHLO failed, try the simpler HELO command.
 	            if (!$this->SmtpSendCommand('HELO', $this->smtpParams['localhost'])) {
 	            	return false;
 	            }
 
 	            if (!$this->SmtpParseResponse(250)) {
 	                return $this->SetError('HELO_ERROR', Array($this->smtpResponceCode), false);
 	            }
 
 	            return true;
 	        }
 
 	        foreach ($this->smtpRespoceArguments as $argument) {
 	            $verb = strtok($argument, ' ');
 	            $arguments = substr($argument, strlen($verb) + 1, strlen($argument) - strlen($verb) - 1);
 	            $this->smtpFeatures[$verb] = $arguments;
 	        }
 
 	        return true;
 	    }
 
 	    /**
 	     * Attempt to do SMTP authentication.
 	     *
 	     * @param string $uid The userid to authenticate as.
 	     * @param string $pwd The password to authenticate with.
 	     * @param string $method The requested authentication method.  If none is specified, the best supported method will be used.
 	     *
 	     * @return bool
 	     */
 	    function SmtpAuthentificate($uid, $pwd , $method = '')
 	    {
 	        if (empty($this->smtpFeatures['AUTH'])) {
 	        	// server doesn't understand AUTH command, then don't authentificate
 	            return true;
 	        }
 
 	        $available_methods = explode(' ', $this->smtpFeatures['AUTH']); // methods supported by SMTP server
 
 	        if (empty($method)) {
 		        foreach ($this->smtpAuthMethods as $supported_method) {
 		        	// check if server supports methods, that we have implemented
 		            if (in_array($supported_method, $available_methods)) {
 		            	$method = $supported_method;
 		                break;
 		            }
 		        }
 	        } else {
 	            $method = strtoupper($method);
 	        }
 
 	        if (!in_array($method, $available_methods)) {
 	        	// coosen method  is not supported by server
 	        	return $this->SetError('AUTH_METHOD_NOT_SUPPORTED', Array($method));
 	        }
 
 	        switch ($method) {
 	        	case 'CRAM-MD5':
 	                $result = $this->_authCRAM_MD5($uid, $pwd);
 	                break;
 
 	            case 'LOGIN':
 	                $result = $this->_authLogin($uid, $pwd);
 	                break;
 	            case 'PLAIN':
 	                $result = $this->_authPlain($uid, $pwd);
 	                break;
 	            default:
 					return $this->SetError('AUTH_METHOD_NOT_IMPLEMENTED', Array($method));
 	                break;
 	        }
 
 			return $result;
 	    }
 
 	    /**
 	    * Function which implements HMAC MD5 digest
 	    *
 	    * @param  string $key  The secret key
 	    * @param  string $data The data to protect
 	    * @return string       The HMAC MD5 digest
 	    */
 	    function _HMAC_MD5($key, $data)
 	    {
 	        if (strlen($key) > 64) {
 	            $key = pack('H32', md5($key));
 	        }
 
 	        if (strlen($key) < 64) {
 	            $key = str_pad($key, 64, chr(0));
 	        }
 
 	        $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64);
 	        $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64);
 
 	        $inner  = pack('H32', md5($k_ipad . $data));
 	        $digest = md5($k_opad . $inner);
 
 	        return $digest;
 	    }
 
 	    /**
 	     * Authenticates the user using the CRAM-MD5 method.
 	     *
 	     * @param string $uid The userid to authenticate as.
 	     * @param string $pwd The password to authenticate with.
 	     *
 	     * @return bool
 	     */
 	    function _authCRAM_MD5($uid, $pwd)
 	    {
 	        if (!$this->SmtpSendCommand('AUTH', 'CRAM-MD5')) {
 	            return false;
 	        }
 
 	        // 334: Continue authentication request
 	        if (!$this->SmtpParseResponse(334)) {
 	            // 503: Error: already authenticated
 	            return $this->smtpResponceCode === 503 ? true : false;
 	        }
 
 	        $challenge = base64_decode($this->smtpRespoceArguments[0]);
 	        $auth_str = base64_encode($uid . ' ' . $this->_HMAC_MD5($pwd, $challenge));
 
 	        if (!$this->SmtpSendCommand($auth_str)) {
 	            return false;
 	        }
 
 	        // 235: Authentication successful
 	        if (!$this->SmtpParseResponse(235)) {
 	            return false;
 	        }
 
 	        return true;
 	    }
 
 	    /**
 	     * Authenticates the user using the LOGIN method.
 	     *
 	     * @param string $uid The userid to authenticate as.
 	     * @param string $pwd The password to authenticate with.
 	     *
 	     * @return bool
 	     */
 	    function _authLogin($uid, $pwd)
 	    {
 	        if (!$this->SmtpSendCommand('AUTH', 'LOGIN')) {
 	            return false;
 	        }
 
 	        // 334: Continue authentication request
 	        if (!$this->SmtpParseResponse(334)) {
 	            // 503: Error: already authenticated
 	            return $this->smtpResponceCode === 503 ? true : false;
 	        }
 
 	        if (!$this->SmtpSendCommand(base64_encode($uid))) {
 	            return false;
 	        }
 
 	        // 334: Continue authentication request
 	        if (!$this->SmtpParseResponse(334)) {
 	            return false;
 	        }
 
 	        if (!$this->SmtpSendCommand(base64_encode($pwd))) {
 	            return false;
 	        }
 
 	        // 235: Authentication successful
 	        if (!$this->SmtpParseResponse(235)) {
 	            return false;
 	        }
 
 	        return true;
 	    }
 
 	    /**
 	     * Authenticates the user using the PLAIN method.
 	     *
 	     * @param string $uid The userid to authenticate as.
 	     * @param string $pwd The password to authenticate with.
 	     *
 	     * @return bool
 	     */
 	    function _authPlain($uid, $pwd)
 	    {
 	        if (!$this->SmtpSendCommand('AUTH', 'PLAIN')) {
 	            return false;
 	        }
 
 	        // 334: Continue authentication request
 	        if (!$this->SmtpParseResponse(334)) {
 	            // 503: Error: already authenticated
 				return $this->smtpResponceCode === 503 ? true : false;
 	        }
 
 	        $auth_str = base64_encode(chr(0) . $uid . chr(0) . $pwd);
 
 	        if (!$this->SmtpSendCommand($auth_str)) {
 	            return false;
 	        }
 
 	        // 235: Authentication successful
 	        if (!$this->SmtpParseResponse(235)) {
 	            return false;
 	        }
 
 	        return true;
 	    }
 
 	    /**
 	     * Send the MAIL FROM: command.
 	     *
 	     * @param string $sender The sender (reverse path) to set.
 	     * @param string $params String containing additional MAIL parameters, such as the NOTIFY flags defined by RFC 1891 or the VERP protocol.
 	     *
 	     * @return bool
 	     */
 	    function SmtpSetFrom($sender, $params = null)
 	    {
 	        $args = "FROM:<$sender>";
 	        if (is_string($params)) {
 	            $args .= ' ' . $params;
 	        }
 
 	        if (!$this->SmtpSendCommand('MAIL', $args)) {
 	            return false;
 	        }
 	        if (!$this->SmtpParseResponse(250)) {
 	            return false;
 	        }
 
 	        return true;
 	    }
 
 	    /**
 	     * Send the RCPT TO: command.
 	     *
 	     * @param string $recipient The recipient (forward path) to add.
 	     * @param string $params String containing additional RCPT parameters, such as the NOTIFY flags defined by RFC 1891.
 	     *
 	     * @return bool
 	     */
 	    function SmtpAddTo($recipient, $params = null)
 	    {
 	        $args = "TO:<$recipient>";
 	        if (is_string($params)) {
 	            $args .= ' ' . $params;
 	        }
 
 	        if (!$this->SmtpSendCommand('RCPT', $args)) {
 	            return false;
 	        }
 
 	        if (!$this->SmtpParseResponse(array(250, 251))) {
 	            return false;
 	        }
 
 	        return true;
 	    }
 
 	    /**
 	     * Send the DATA command.
 	     *
 	     * @param string $data The message body to send.
 	     *
 	     * @return bool
 	     */
 	    function SmtpSendMessage($data)
 	    {
 	        /* RFC 1870, section 3, subsection 3 states "a value of zero
 	         * indicates that no fixed maximum message size is in force".
 	         * Furthermore, it says that if "the parameter is omitted no
 	         * information is conveyed about the server's fixed maximum
 	         * message size". */
 	        if (isset($this->smtpFeatures['SIZE']) && ($this->smtpFeatures['SIZE'] > 0)) {
 	            if (strlen($data) >= $this->smtpFeatures['SIZE']) {
 	                $this->SmtpDisconnect();
 	                return $this->SetError('Message size excedes the server limit', null, false);
 	            }
 	        }
 
 	        // Quote the data based on the SMTP standards
 
 	        // Change Unix (\n) and Mac (\r) linefeeds into Internet-standard CRLF (\r\n) linefeeds.
 	        $data = preg_replace(Array('/(?<!\r)\n/','/\r(?!\n)/'), "\r\n", $data);
 
 	        // Because a single leading period (.) signifies an end to the data,
 	        // legitimate leading periods need to be "doubled" (e.g. '..')
 	        $data = str_replace("\n.", "\n..", $data);
 
 	        if (!$this->SmtpSendCommand('DATA')) {
 	            return false;
 	        }
 	        if (!$this->SmtpParseResponse(354)) {
 	            return false;
 	        }
 
 	        if ($this->smtpSocket->write($data . "\r\n.\r\n") === false) {
 	            return false;
 	        }
 	        if (!$this->SmtpParseResponse(250)) {
 	            return false;
 	        }
 
 	        return true;
 	    }
 
 		/**
 		 * Sets global charset for every message part
 		 *
 		 * @param string $charset
 		 * @param bool $is_system set charset to default for current language
 		 */
 		function SetCharset($charset, $is_system = false)
 		{
 			$this->charset = $is_system ? CHARSET : $charset;
 		}
 
 		/**
 		 * Allows to extract recipient's name from text by specifying it's email
 		 *
 		 * @param string $text
 		 * @param string $email
 		 * @return string
 		 */
 		function ExtractRecipientName($text, $email = '')
 		{
 			$lastspace = mb_strrpos($text, ' ');
 			$name = trim(mb_substr($text, 0, $lastspace - mb_strlen($text)), " \r\n\t\0\x0b\"'");
 			if (empty($name)) {
 				$name = $email;
 			}
 			return $name;
 		}
 
 		/**
 		 * Takes $text and returns an email address from it
 		 * Set $multiple to true to retrieve all found addresses
 		 * Returns false if no addresses were found
 		 *
 		 * @param string $text
 		 * @param bool $multiple
 		 * @param bool $allow_only_domain
 		 * @return Array|bool
 		 * @access public
 		 */
 		public function ExtractRecipientEmail($text, $multiple = false, $allow_only_domain = false)
 		{
 			if ( $allow_only_domain ) {
 				$pattern = '/((' . REGEX_EMAIL_USER . '@)?' . REGEX_EMAIL_DOMAIN . ')/i';
 			}
 			else {
 				$pattern = '/(' . REGEX_EMAIL_USER . '@' . REGEX_EMAIL_DOMAIN . ')/i';
 			}
 			if ( $multiple ) {
 				if ( preg_match_all($pattern, $text, $found_emails) >= 1 ) {
 					return $found_emails[1];
 				}
 				else {
 					return false;
 				}
 			}
 			else {
 				if ( preg_match($pattern, $text, $found_emails) == 1 ) {
 					return $found_emails[1];
 				}
 				else {
 					return false;
 				}
 			}
 		}
 
 		/**
 		 * Returns array of recipient names and emails
 		 *
 		 * @param string $list
 		 * @param string $separator
 		 * @return Array
 		 */
 		function GetRecipients($list, $separator = ';')
 		{
 			// by MIME specs recipients should be separated using "," symbol,
 			// but users can write ";" too (like in OutLook)
 
 			if (!trim($list)) {
 				return false;
 			}
 
 			$list = explode(',', str_replace($separator, ',', $list));
 
 			$ret = Array ();
 			foreach ($list as $recipient) {
 				$email = $this->ExtractRecipientEmail($recipient);
 				if (!$email) {
 					// invalid email format -> error
 					return false;
 				}
 				$name = $this->ExtractRecipientName($recipient, $email);
 				$ret[] = Array('Name' => $name, 'Email' => $email);
 			}
 
 			return $ret;
 		}
 
 		/* methods for nice header setting */
 
 		/**
 		 * Sets "From" header.
 		 *
 		 * @param string $email
 		 * @param string $first_last_name FirstName and LastName or just FirstName
 		 * @param string $last_name LastName (if not specified in previous parameter)
 		 */
 		function SetFrom($email, $first_last_name, $last_name = '')
 		{
 			$name = rtrim($first_last_name.' '.$last_name, ' ');
 			$this->SetEncodedEmailHeader('From', $email, $name ? $name : $email);
 
 			if (!isset($this->headers['Return-Path'])) {
 				$this->SetReturnPath($email);
 			}
 		}
 
 		/**
 		 * Sets "To" header.
 		 *
 		 * @param string $email
 		 * @param string $first_last_name FirstName and LastName or just FirstName
 		 * @param string $last_name LastName (if not specified in previous parameter)
 		 */
 		function SetTo($email, $first_last_name, $last_name = '')
 		{
 			$name = rtrim($first_last_name.' '.$last_name, ' ');
 			$email = $this->_replaceRecipientEmail($email);
 
 			$this->SetEncodedEmailHeader('To', $email, $name ? $name : $email);
 		}
 
 		/**
 		 * Sets "Return-Path" header (useful for spammers)
 		 *
 		 * @param string $email
 		 */
 		function SetReturnPath($email)
 		{
 			$this->SetHeader('Return-Path', $email);
 		}
 
 		/**
 		 * Adds one more recipient into "To" header
 		 *
 		 * @param string $email
 		 * @param string $first_last_name FirstName and LastName or just FirstName
 		 * @param string $last_name LastName (if not specified in previous parameter)
 		 */
 		function AddTo($email, $first_last_name = '', $last_name = '')
 		{
 			$name = rtrim($first_last_name.' '.$last_name, ' ');
 			$this->AddRecipient('To', $email, $name);
 		}
 
 		/**
 		 * Allows to replace recipient in all sent emails (used for debugging)
 		 *
 		 * @param string $email
 		 * @return string
 		 */
 		function _replaceRecipientEmail($email)
 		{
 			if ( defined('OVERRIDE_EMAIL_RECIPIENTS') && OVERRIDE_EMAIL_RECIPIENTS ) {
 				if ( substr(OVERRIDE_EMAIL_RECIPIENTS, 0, 1) == '@' ) {
 					// domain
 					$email = str_replace('@', '_at_', $email) . OVERRIDE_EMAIL_RECIPIENTS;
 				}
 				else {
 					$email = OVERRIDE_EMAIL_RECIPIENTS;
 				}
 			}
 
 			return $email;
 		}
 
 		/**
 		 * Adds one more recipient into "Cc" header
 		 *
 		 * @param string $email
 		 * @param string $first_last_name FirstName and LastName or just FirstName
 		 * @param string $last_name LastName (if not specified in previous parameter)
 		 */
 		function AddCc($email, $first_last_name = '', $last_name = '')
 		{
 			$name = rtrim($first_last_name.' '.$last_name, ' ');
 			$this->AddRecipient('Cc', $email, $name);
 		}
 
 		/**
 		 * Adds one more recipient into "Bcc" header
 		 *
 		 * @param string $email
 		 * @param string $first_last_name FirstName and LastName or just FirstName
 		 * @param string $last_name LastName (if not specified in previous parameter)
 		 */
 		function AddBcc($email, $first_last_name = '', $last_name = '')
 		{
 			$name = rtrim($first_last_name.' '.$last_name, ' ');
 			$this->AddRecipient('Bcc', $email, $name);
 		}
 
 		/**
 		 * Adds one more recipient to specified header
 		 *
 		 * @param string $header_name
 		 * @param string $email
 		 * @param string $name
 		 */
 		function AddRecipient($header_name, $email, $name = '')
 		{
 			$email = $this->_replaceRecipientEmail($email);
 
 			if (!$name) {
 				$name = $email;
 			}
 
 			$value = isset($this->headers[$header_name]) ? $this->headers[$header_name] : '';
 
 			if ( $value ) {
 				// not first recipient added - separate with comma
 				$value .= ', ';
 			}
 
 			$value .= $this->QuotedPrintableEncode($name, $this->charset) . ' <' . $email . '>';
 			$this->SetHeader($header_name, $value);
 		}
 
 		/**
 		 * Returns list of recipients from given header.
 		 *
 		 * @param string $header_name Header name.
 		 *
 		 * @return array
 		 */
 		public function GetRecipientsByHeader($header_name)
 		{
 			if ( !isset($this->headers[$header_name]) ) {
 				return array();
 			}
 
 			$decoded_header = $this->decodeHeader($this->headers[$header_name]);
 			$recipients = $this->GetRecipients($decoded_header);
 
 			if ( $recipients === false ) {
 				return array();
 			}
 
 			return $recipients;
 		}
 
 		/**
 		 * Decodes header value.
 		 *
 		 * @param string $header_value Header value.
 		 *
 		 * @return string
 		 */
 		protected function decodeHeader($header_value)
 		{
 			while ( preg_match('/(=\?([^?]+)\?(Q|B)\?([^?]*)\?=)/i', $header_value, $matches) ) {
 				$encoded = $matches[1];
 				$charset = $matches[2];
 				$encoding = $matches[3];
 				$text = $matches[4];
 
 				switch ( strtoupper($encoding) ) {
 					case 'B':
 						$text = base64_decode($text);
 						break;
 
 					case 'Q':
 						$text = str_replace('_', ' ', $text);
 						preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
 
 						foreach ( $matches[1] as $value ) {
 							$text = str_replace('=' . $value, chr(hexdec($value)), $text);
 						}
 						break;
 				}
 
 				$header_value = mb_convert_encoding(
 					str_replace($encoded, $text, $header_value),
 					$this->charset,
 					$charset
 				);
 			}
 
 			return $header_value;
 		}
 
 		/**
 		 * Sets "Subject" header.
 		 *
 		 * @param string $subject message subject
 		 */
 		function SetSubject($subject)
 		{
 			$this->setEncodedHeader('Subject', $subject);
 		}
 
 		/**
 		 * Sets HTML part of message
 		 *
 		 * @param string $html
 		 */
 		function SetHTML($html)
 		{
 			$this->CreateTextHtmlPart($html, true);
 		}
 
 		/**
 		 * Sets Plain-Text part of message
 		 *
 		 * @param string $plain_text
 		 */
 		function SetPlain($plain_text)
 		{
 			$this->CreateTextHtmlPart($plain_text);
 		}
 
 		/**
 		 * Sets HTML and optionally plain part of the message
 		 *
 		 * @param string $html
 		 * @param string $plain_text
 		 */
 		function SetBody($html, $plain_text = '')
 		{
 			$this->SetHTML($html);
 			if ($plain_text) {
 				$this->SetPlain($plain_text);
 			}
 		}
 
 		/**
 		 * Performs mail delivery (supports delayed delivery)
 		 *
 		 * @param string $message message, if not given, then use composed one
 		 * @param bool $immediate_send send message now or MailingId
 		 * @param bool $immediate_clear clear message parts after message is sent
 		 * @return bool
 		 */
 		function Deliver($message = null, $immediate_send = true, $immediate_clear = true)
 		{
 			if (isset($message)) {
 				// if message is given directly, then use it
 				if (is_array($message)) {
 					$message_headers =& $message[0];
 					$message_body =& $message[1];
 				}
 				else {
 					$message_headers = Array ();
 					list ($headers, $message_body) = explode("\n\n", $message, 2);
 					$headers = explode("\n", $headers);
 					foreach ($headers as $header) {
 						$header = explode(':', $header, 2);
 						$message_headers[ trim($header[0]) ] = trim($header[1]);
 					}
 				}
 				$composed = true;
 			} else {
 				// direct message not given, then assemble message from available parts
 				$composed = $this->GetHeadersAndBody($message_headers, $message_body);
 			}
 
 			if ( $composed ) {
 				if ( $immediate_send === true ) {
 					$send_method = 'Send' . $this->sendMethod;
 					$result = $this->$send_method($message_headers, $message_body);
 
 					if ( $result && $this->_logData ) {
 						// add e-mail log record
 						$this->Conn->doInsert($this->_logData, TABLE_PREFIX . 'EmailLog');
 					}
 
 					if ( $immediate_clear ) {
 						$this->Clear();
 					}
 
 					return $result;
 				}
 				else {
 					$fields_hash = Array (
 						'ToEmail' => $message_headers['To'],
 						'Subject' => $message_headers['Subject'],
 						'Queued' => adodb_mktime(),
 						'SendRetries' => 0,
 						'LastSendRetry' => 0,
 						'MailingId' => (int)$immediate_send,
 						'LogData' => serialize($this->_logData), // remember e-mail log record
 					);
 
 					$fields_hash['MessageHeaders'] = serialize($message_headers);
 					$fields_hash['MessageBody'] =& $message_body;
 					$this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'EmailQueue');
 
 					if ( $immediate_clear ) {
 						$this->Clear();
 					}
 				}
 			}
 
 			// if not immediate send, then send result is positive :)
 			return $immediate_send !== true ? true : false;
 		}
 
 		/**
 		 * Sets log data
 		 *
 		 * @param string $log_data
 		 * @return void
 		 * @access public
 		 */
 		public function setLogData($log_data)
 		{
 			$this->_logData = $log_data;
 		}
 	}