Array (),
EmailTemplate::RECIPIENT_TYPE_CC => Array (),
EmailTemplate::RECIPIENT_TYPE_BCC => Array (),
);
/**
* Stores log data.
*
* @var array
*/
protected $logData = 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->logData = array();
$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',
'PrefixSpecial', 'item_id',
);
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
try {
$message_headers = $this->_getHeaders();
}
catch ( Exception $e ) {
return $this->setError('Error parsing e-mail message headers');
}
$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() ) {
// 1. prepare log
$this->logData = Array (
'From' => $this->fromName . ' (' . $this->fromEmail . ')',
'To' => $this->toName . ' (' . $this->toEmail . ')',
'OtherRecipients' => serialize($this->recipients),
'Subject' => $message_subject,
'Status' => EmailLogStatus::SENT,
'ErrorMessage' => '',
'SentOn' => TIMENOW,
'TemplateName' => $this->emailTemplate->GetDBField('TemplateName'),
'EventType' => $this->emailTemplate->GetDBField('Type'),
'EventParams' => serialize($this->_getCustomParams()),
'ToUserId' => $this->recipientUserId,
'ItemPrefix' => $this->getItemPrefix(),
'ItemId' => isset($this->params['item_id']) ? $this->params['item_id'] : null,
);
$this->params['email_access_key'] = $this->_generateAccessKey();
}
// 3. set body
try {
$html_message_body = $this->_getMessageBody(true);
$plain_message_body = $this->_getMessageBody(false);
}
catch ( Exception $e ) {
return $this->setError('Error parsing e-mail message body');
}
if ( $html_message_body === false && $plain_message_body === false ) {
return $this->setError('Message template is empty (maybe after parsing).');
}
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
$this->logData['HtmlBody'] = $html_message_body;
$this->logData['TextBody'] = $plain_message_body;
$this->logData['AccessKey'] = $this->params['email_access_key'];
$this->sender->setLogData($this->logData);
}
$delivery = isset($this->params['delivery']) ? $this->params['delivery'] : $this->Application->ConfigValue('EmailDelivery');
return $this->sender->Deliver(null, $delivery == EmailDelivery::IMMEDIATE);
}
/**
* Extracts prefix from a given PrefixSpecial parameter.
*
* @return string
*/
protected function getItemPrefix()
{
$prefix_special = isset($this->params['PrefixSpecial']) ? $this->params['PrefixSpecial'] : '';
if ( !$prefix_special ) {
return '';
}
$prefix_info = $this->Application->processPrefix($prefix_special);
return $prefix_info['prefix'];
}
/**
* Determines whatever we should keep e-mail log or not
*
* @return bool
* @access protected
*/
protected function _storeEmailLog()
{
return $this->Application->ConfigValue('EnableEmailLog');
}
/**
* Marks e-mail sending as failed.
*
* @param string $error_message Error message.
*
* @return boolean
*/
protected function setError($error_message)
{
if ( $this->_storeEmailLog() ) {
$this->logData['Status'] = EmailLogStatus::ERROR;
$this->logData['ErrorMessage'] = $error_message;
$this->Conn->doInsert($this->logData, TABLE_PREFIX . 'EmailLog');
}
return false;
}
/**
* Generates access key for accessing e-mail later
*
* @return string
* @access protected
*/
protected function _generateAccessKey()
{
$ret = '';
$use_fields = Array ('From', 'To', 'Subject');
foreach ($use_fields as $use_field) {
$ret .= $this->logData[$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']);
/** @var UsersItem $user */
$user = $this->Application->recallObject(
'u.email-from',
null,
array('live_table' => true, 'skip_autoload' => true)
);
$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 = $this->getHeaderMapping();
$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();
$this->_moveDirectRecipients();
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;
}
/** @var MInputHelper $minput_helper */
$minput_helper = $this->Application->recallObject('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);
/** @var UsersItem $user */
$user = $this->Application->recallObject(
'u.email-to',
null,
array('live_table' => true, 'skip_autoload' => true)
);
$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);
}
}
/**
* Move recipients, that were added manually via "$this->sender->Add*" methods.
*
* @return void
* @access protected
*/
protected function _moveDirectRecipients()
{
foreach ( $this->getHeaderMapping() as $recipient_type => $header_name ) {
$manual_recipients = $this->sender->GetRecipientsByHeader($header_name);
if ( !$manual_recipients ) {
continue;
}
foreach ( $manual_recipients as $manual_recipient ) {
$this->recipients[$recipient_type][] = array(
'RecipientName' => $manual_recipient['Name'],
'RecipientAddressType' => EmailTemplate::ADDRESS_TYPE_EMAIL,
'RecipientAddress' => $manual_recipient['Email'],
);
}
$this->sender->SetHeader($header_name, '');
}
}
/**
* Returns mapping between recipient type and header name.
*
* @return array
*/
protected function getHeaderMapping()
{
return array(
EmailTemplate::RECIPIENT_TYPE_TO => 'To',
EmailTemplate::RECIPIENT_TYPE_CC => 'Cc',
EmailTemplate::RECIPIENT_TYPE_BCC => 'Bcc',
);
}
/**
* 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);
/** @var LanguagesItem $language */
$language = $this->Application->recallObject('lang.current');
$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]) ) {
/** @var LanguagesItem $language */
$language = $this->Application->recallObject('lang.current');
$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);
$template_name = 'et_' . $this->emailTemplate->GetID() . '_' . crc32($text);
$text = $this->Application->Parser->Parse($this->_normalizeLineEndings($text), $template_name);
$this->Application->Parser->SetParams($parser_params); // restore parser params
/** @var CategoryHelper $category_helper */
$category_helper = $this->Application->recallObject('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);
}
}