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