Files
IPB/system/Email/Email.php
T
2025-12-19 04:57:54 -08:00

1195 lines
37 KiB
PHP

<?php
/**
* @brief Outgoing Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Email Class
*/
abstract class _Email
{
/**
* @brief Temporary store needed in IN_DEV to rememver what parameters a template has
*/
protected static $matchesStore = '';
/**
* @brief Headers
*/
public $headers = array(
'MIME-Version' => '1.0',
'Precedence' => 'list', /* This is to try to prevent auto-responders and delivery failure notifications from responding */
'Auto-Submitted' => 'auto-generated' /* This is to try to prevent auto-responders and delivery failure notifications from responding */
);
/**
* @brief From address
*/
public $from = '';
/**
* @brief From display name
*/
public $fromName = '';
/**
* @brief "To" addresses not encoded, primarily used for unsubscribe and debug purposes
*/
public $to = array();
/**
* @brief All Recipients
*/
protected $recipients = array();
/**
* @brief Subject
*/
public $subject = '';
/**
* @brief Message (this is the full compiled message complete with content-type and boundary markers)
*/
protected $message = '';
/**
* @brief Unsubscribe link and text
*/
public $unsubscribe = array( 'blurb' => '', 'link' => '', 'text' => '' );
/**
* @brief Wrap in our main template wrapper
*/
public $useWrapper = TRUE;
/**
* @brief Email template information
*/
public $emailTemplate = array();
/**
* @brief Raw email body
*/
public $emailBody = array( 'html' => '', 'plain' => '' );
/**
* @brief Language object use
*/
public $language = NULL;
/**
* @brief Line Endings to use
*/
public $eol = "\r\n";
/**
* Build an unsubscribe link
*
* @param string $type Type of unsubscribe link to automatically build. Valid values are 'bulk' and 'notification'.
* @param string|\IPS\Member $data Data necessary for unsubscribe link. For 'bulk' type this is a member object. For 'notification' type this is the notification key.
* @return \IPS\Email
*/
public function buildUnsubscribe( $type, $data )
{
if( $type == 'bulk' AND $data instanceof \IPS\Member )
{
$this->unsubscribe['text'] = 'unsubscribe';
$this->unsubscribe['link'] = \IPS\Http\Url::internal( 'app=core&module=system&controller=unsubscribe&email=' . $data->email . '&key=' . md5( $data->email . ':' . $data->members_pass_hash ), 'front', 'unsubscribe' );
}
else if( $type == 'notification' )
{
$this->unsubscribe['text'] = 'adjust_notification_prefs';
$this->unsubscribe['link'] = \IPS\Http\Url::internal( 'app=core&module=system&controller=notifications&do=options&type=' . (string) $data , 'front', 'notifications_options' );
}
elseif ( $type == 'follow' )
{
$this->unsubscribe['blurb'] = '*|unsubscribe_blurb|*';
$this->unsubscribe['text'] = 'unfollow';
$this->unsubscribe['link'] = '*|unfollow_link|*';
$this->unsubscribe['ortext']= 'adjust_notification_prefs';
$this->unsubscribe['orlink']= \IPS\Http\Url::internal( 'app=core&module=system&controller=notifications&do=options&type=' . (string) $data , 'front', 'notifications_options' );
}
return $this;
}
/**
* Set the email body
*
* @param string $name Name of the property
* @param array $value Value of the property (should be an array with keys 'html' and 'plain')
* @return array ('html' and 'plain')
*/
public function __set( $name, $value )
{
if( $name == 'body' )
{
$this->emailBody = $value;
}
}
/**
* Retrieve the email body - factors in template and wrapper as needed
*
* @param string $name Name of the property
* @return array ('html' and 'plain')
*/
public function __get( $name )
{
if( $name == 'body' )
{
/* Do we have to build the email from a template? */
if( count($this->emailTemplate) AND !$this->emailBody['html'] AND !$this->emailBody['plain'] )
{
$this->parseTemplate();
}
/* Do we have to wrap the email in our wrapper? */
if( $this->useWrapper )
{
return $this->wrapEmail();
}
else
{
return $this->emailBody;
}
}
}
/**
* Determine if we have a specific email template
*
* @param string $app Application key
* @param string $key Email template key
* @return bool
*/
public static function hasTemplate( $app, $key )
{
if( \IPS\IN_DEV )
{
foreach ( array( 'phtml', 'txt' ) as $type )
{
if( file_exists( \IPS\ROOT_PATH . "/applications/{$app}/dev/email/{$key}.{$type}" ) )
{
return TRUE;
}
}
return FALSE;
}
else
{
/* See if we found anything from the store */
$key = md5( $app . ';' . $key ) . '_email_html';
$htmlTemplate = ( isset( \IPS\Data\Store::i()->$key ) ) ? \IPS\Data\Store::i()->$key : NULL;
if( $htmlTemplate )
{
return TRUE;
}
$key = md5( $app . ';' . $key ) . '_email_plaintext';
$plaintextTemplate = ( isset( \IPS\Data\Store::i()->$key ) ) ? \IPS\Data\Store::i()->$key : NULL;
if( $plaintextTemplate )
{
return TRUE;
}
return FALSE;
}
}
/**
* Initiate a new custom email based on raw email content
*
* @note While both body parameters are optional, you should really specify at least one, otherwise there will be no email body unless you manually set the body later.
* @param string $subject Subject
* @param string $html HTML Version
* @param string $plain Plaintext version
* @return \IPS\Email
*/
public static function buildFromContent( $subject, $htmlContent='', $plaintextContent='' )
{
if( !$plaintextContent AND $htmlContent )
{
$plaintextContent = static::buildPlaintextBody( $htmlContent );
}
$obj = static::i( $htmlContent, $plaintextContent );
$obj->subject = $subject;
return $obj;
}
/**
* Turn an HTML email into a plaintext email
*
* @param string $html HTML email
* @return string
* @note We might find that using HTML Purifier to retain links in parenthesis is useful.
*/
public static function buildPlaintextBody( $html )
{
/* Add newlines as needed */
$html = str_replace( "</p>", "</p>\n", $html );
$html = str_replace( array( "<br>", "<br />" ), "\n", $html );
/* Strip HTML and return */
return strip_tags( $html );
}
/**
* Initiate new email using a template
*
* @param string $app Application key
* @param string $key Email template key
* @param array $parameters Parameters for the template
* @param bool $placeholderRecipient If TRUE, will use placeholders where member data will be rather than an actual member. Use if you will send using mergeAndSend
* @return \IPS\Email
*/
public static function buildFromTemplate( $app, $key, $parameters=array(), $placeholderRecipient=FALSE )
{
$email = static::i();
$email->emailTemplate = array(
'app' => $app,
'key' => $key,
'params' => $parameters,
'placeholderRecipient' => $placeholderRecipient,
);
return $email;
}
/**
* Initiate new email from headers and body as an array
*
* @param array $headers Email headers
* @param array $body Email body array
* @return \IPS\Email
*/
public static function buildFromRaw( $headers, $body )
{
$email = static::i();
foreach( $headers as $k => $v )
{
$email->headers[ $k ] = $v;
}
foreach( $body as $k => $v )
{
$email->emailBody[ $k ] = $v;
}
return $email;
}
/**
* Factory Method
*
* @param string $html HTML Version
* @param string $plain Plaintext version
* @return \IPS\Email
*/
protected static function i( $html='', $plain='' )
{
/* What email handler are we using? */
if( defined( '\IPS\EMAIL_DEBUG_PATH' ) AND \IPS\EMAIL_DEBUG_PATH )
{
$email = new \IPS\Email\Outgoing\Debug;
$email->debugPath = \IPS\EMAIL_DEBUG_PATH;
}
else if ( \IPS\Settings::i()->mail_method === 'smtp' )
{
$email = new \IPS\Email\Outgoing\Smtp;
}
else
{
$email = new \IPS\Email\Outgoing\Php;
}
/**
* PHP mail() line endings within email headers need to match the operating system default, which then passes the mail off to the local MTA which then replaces them if needed.
* @see <a href='https://bugs.php.net/bug.php?id=15841'>PHP Bug 15841</a>
*/
$email->eol = ( \IPS\Settings::i()->mail_method === 'smtp' ) ? "\r\n" : PHP_EOL;
/* Set a default language - this is overridden by the language of the recipient in the send() method if an instance of \IPS\Member is passed */
$email->language = \IPS\Lang::load( \IPS\Lang::defaultLanguage() );
/* Set a common default headers */
$email->headers['Date'] = date('r');
$email->from = \IPS\Settings::i()->email_out;
$email->fromName = \IPS\Settings::i()->board_name;
/* Store the body */
$email->emailBody = array( 'html' => $html ?: $plain, 'plain' => $plain ?: static::buildPlaintextBody( $html ) );
/* Return */
return $email;
}
/**
* Compile Headers
*
* @see <a href='http://www.ietf.org/rfc/rfc2822.txt'>RFC 2822</a>
* @see <a href='http://www.ietf.org/rfc/rfc2045.txt'>RFC 2045</a>
* @param bool $isPhp If using the PHP handler we need to skip the to and subject values
* @return string
*/
protected function _compileHeaders( $isPhp=FALSE )
{
$this->headers['From'] = ( $this->fromName ) ? ( '=?UTF-8?B?' . base64_encode( $this->fromName ) . "?= <{$this->from}>" ) : "<{$this->from}>";
if( !$isPhp )
{
$this->headers['Subject'] = mb_encode_mimeheader( $this->subject );
}
/* Set the headers and return */
$return = array();
foreach ( $this->headers as $k => $v )
{
if( \strtolower( $k ) == 'to' AND $isPhp )
{
continue;
}
/* We have already encoded the email address fields, as they have to be encoded differently */
if( !in_array( \strtolower( $k ), array( 'from', 'to', 'cc', 'bcc', 'subject' ) ) )
{
/* If there are non-ASCII characters we need to encode them */
if( preg_match('#([\x80-\xFF]){1}#', $v ) )
{
$v = mb_encode_mimeheader( $v );
}
/* Otherwise we may still need to perform case-folding */
else if( \strlen( "{$k}: {$v}" ) > 78 )
{
$v = \substr( wordwrap( "{$k}: {$v}", 78, "{$this->eol} " ), \strlen( $k ) + 2 );
}
}
$return[] = "{$k}: {$v}";
}
return implode( "{$this->eol}", $return );
}
/**
* Build the email body and subject from a template
*
* @return void
* @throws OutOfRangeException
* @note This method can throw an exception if you call an invalid template
*/
public function parseTemplate()
{
if( !count( $this->emailTemplate ) )
{
return;
}
$_typeMap = array( 'phtml' => 'html', 'txt' => 'plain' );
$params = array_merge( $this->emailTemplate['params'], array( $this ) );
if( \IPS\IN_DEV )
{
/* Parse templates */
foreach ( array( 'phtml', 'txt' ) as $type )
{
$this->emailBody[ $_typeMap[ $type ] ] = static::devProcessTemplate( "email__{$this->emailTemplate['app']}_{$this->emailTemplate['key']}_{$type}", file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->emailTemplate['app']}/dev/email/{$this->emailTemplate['key']}.{$type}" ), $params );
}
}
else
{
/* Get built copy */
if( empty( $this->emailBody['html'] ) )
{
$key = md5( $this->emailTemplate['app'] . ';' . $this->emailTemplate['key'] ) . '_email_html';
if ( \IPS\Data\Store::i()->exists( $key ) === FALSE )
{
$templateData = \IPS\Db::i()->select( '*', 'core_email_templates', array( "template_app=? AND template_name=?", $this->emailTemplate['app'], $this->emailTemplate['key'] ) )->first();
\IPS\Data\Store::i()->$key = "namespace IPS\Theme;\n" . \IPS\Theme::compileTemplate( $templateData['template_content_html'], "email_html_{$templateData['template_app']}_{$templateData['template_name']}", $templateData['template_data'] );
}
$template = \IPS\Data\Store::i()->$key;
if( !function_exists( 'IPS\\Theme\\email_html_' . $this->emailTemplate['app'] . '_' . $this->emailTemplate['key'] ) )
{
eval( $template );
}
$this->emailBody['html'] = call_user_func_array( 'IPS\\Theme\\email_html_' . $this->emailTemplate['app'] . '_' . $this->emailTemplate['key'], $params );
}
if( empty( $this->emailBody['plain'] ) )
{
$key = md5( $this->emailTemplate['app'] . ';' . $this->emailTemplate['key'] ) . '_email_plaintext';
if ( \IPS\Data\Store::i()->exists( $key ) === FALSE )
{
$templateData = \IPS\Db::i()->select( '*', 'core_email_templates', array( "template_app=? AND template_name=?", $this->emailTemplate['app'], $this->emailTemplate['key'] ) )->first();
\IPS\Data\Store::i()->$key = "namespace IPS\\Theme;\n" . \IPS\Theme::compileTemplate( $templateData['template_content_plaintext'], "email_plaintext_{$templateData['template_app']}_{$templateData['template_name']}", $templateData['template_data'] );
}
$template = \IPS\Data\Store::i()->$key;
if( !function_exists( 'IPS\\Theme\\email_plaintext_' . $this->emailTemplate['app'] . '_' . $this->emailTemplate['key'] ) )
{
eval( $template );
}
$this->emailBody['plain'] = call_user_func_array( 'IPS\\Theme\\email_plaintext_' . $this->emailTemplate['app'] . '_' . $this->emailTemplate['key'], $params );
}
}
/* Get subject */
$subjectKey = "mailsub__{$this->emailTemplate['app']}_{$this->emailTemplate['key']}";
$this->subject = trim( static::devProcessTemplate( "email__{$this->emailTemplate['app']}_{$this->emailTemplate['key']}_subject", $this->language->get( $subjectKey ), $params ) );
}
/**
* Wrap the email body in the wrapper template if necessary
*
* @return array Array containing the new wrapped html and plaintext emails
* @throws OutOfRangeException
* @note This method can throw an exception if the wrapper template is not compiled
*/
public function wrapEmail()
{
/* We need the member we are sending to */
$member = NULL;
if( !empty( $this->to ) )
{
try
{
$member = \IPS\Member::load( $this->to[0], 'email' );
/* Language may already be set from parseRecipients */
if( $this->language === NULL )
{
$this->language = $member->language( TRUE );
}
}
catch( \OutOfRangeException $e )
{
$member = NULL;
}
}
if( $member === NULL )
{
$member = new \IPS\Member;
}
/* Wrap as appropriate */
if( \IPS\IN_DEV )
{
$body = array();
/* Wrap the HTML version in the wrapper */
if( !empty( $this->emailBody['html'] ) )
{
$body['html'] = static::devProcessTemplate( "email_html_core_emailWrapper", file_get_contents( \IPS\ROOT_PATH . "/applications/core/dev/email/emailWrapper.phtml" ), array( $this->subject, $member, $this->emailBody['html'], $this->unsubscribe, isset( $this->emailTemplate['placeholderRecipient'] ) ? $this->emailTemplate['placeholderRecipient'] : FALSE, '', $this ) );
}
/* Wrap the Plaintext version in the wrapper */
if( !empty( $this->emailBody['plain'] ) )
{
$body['plain'] = static::devProcessTemplate( "email_plaintext_core_emailWrapper", file_get_contents( \IPS\ROOT_PATH . "/applications/core/dev/email/emailWrapper.txt" ), array( $this->subject, $member, $this->emailBody['plain'], $this->unsubscribe, isset( $this->emailTemplate['placeholderRecipient'] ) ? $this->emailTemplate['placeholderRecipient'] : FALSE, '', $this ) );
}
}
else
{
/* Get built copy */
if( !empty( $this->emailBody['html'] ) )
{
$key = md5( 'core;emailWrapper' ) . '_email_html';
if ( \IPS\Data\Store::i()->exists( $key ) === FALSE )
{
$templateData = \IPS\Db::i()->select( '*', 'core_email_templates', array( "template_app=? AND template_name=?", 'core', 'emailWrapper' ) )->first();
\IPS\Data\Store::i()->$key = "namespace IPS\Theme;\n" . \IPS\Theme::compileTemplate( $templateData['template_content_html'], "email_html_core_emailWrapper", $templateData['template_data'] );
}
$template = \IPS\Data\Store::i()->$key;
if( !function_exists( 'IPS\\Theme\\email_html_core_emailWrapper' ) )
{
eval( $template );
}
$body['html'] = call_user_func_array( 'IPS\\Theme\\email_html_core_emailWrapper', array( $this->subject, $member, $this->emailBody['html'], $this->unsubscribe, isset( $this->emailTemplate['placeholderRecipient'] ) ? $this->emailTemplate['placeholderRecipient'] : FALSE, '', $this ) );
}
if( !empty( $this->emailBody['plain'] ) )
{
$key = md5( 'core;emailWrapper' ) . '_email_plaintext';
if ( \IPS\Data\Store::i()->exists( $key ) === FALSE )
{
$templateData = \IPS\Db::i()->select( '*', 'core_email_templates', array( "template_app=? AND template_name=?", 'core', 'emailWrapper' ) )->first();
\IPS\Data\Store::i()->$key = "namespace IPS\Theme;\n" . \IPS\Theme::compileTemplate( $templateData['template_content_plaintext'], "email_plaintext_core_emailWrapper", $templateData['template_data'] );
}
$template = \IPS\Data\Store::i()->$key;
if( !function_exists( 'IPS\\Theme\\email_plaintext_core_emailWrapper' ) )
{
eval( $template );
}
$body['plain'] = call_user_func_array( 'IPS\\Theme\\email_plaintext_core_emailWrapper', array( $this->subject, $member, $this->emailBody['plain'], $this->unsubscribe, isset( $this->emailTemplate['placeholderRecipient'] ) ? $this->emailTemplate['placeholderRecipient'] : FALSE, '', $this ) );
}
}
return $body;
}
/**
* Send
*
* @param mixed $to The member or email address, or array of members or email addresses, to send to
* @param mixed $cc Addresses to CC (can also be email, member or array of either)
* @param mixed $bcc Addresses to BCC (can also be email, member or array of either)
* @return bool
* @throws \IPS\Email\Outgoing\Exception
* @see <a href='http://www.faqs.org/rfcs/rfc2369.html'>RFC 2369 describes the List-Unsubscribe header</a>
*/
public function send( $to, $cc=array(), $bcc=array() )
{
/* Parse the recipients - do this first in order to extract the language key in case we need it */
$this->recipients = array();
$this->headers['To'] = $this->_parseRecipients( $to, TRUE );
if ( !empty( $cc ) )
{
$this->headers['Cc'] = $this->_parseRecipients( $cc );
}
elseif ( isset( $this->headers['Cc'] ) )
{
unset( $this->headers['Cc'] );
}
if ( !empty( $bcc ) )
{
$this->headers['Bcc'] = $this->_parseRecipients( $bcc );
}
elseif ( isset( $this->headers['Bcc'] ) )
{
unset( $this->headers['Bcc'] );
}
/* If we are appending an unsubscribe link, add the List-Unsubscribe header */
if( !empty( $this->unsubscribe['link'] ) )
{
$this->headers['List-Unsubscribe'] = $this->unsubscribe['link'];
}
/* Do we have to build the email from a template? */
if( count($this->emailTemplate) AND !$this->emailBody['html'] AND !$this->emailBody['plain'] )
{
$this->parseTemplate();
}
/* Do we have to wrap the email in our wrapper? */
if( $this->useWrapper )
{
$this->emailBody = $this->wrapEmail();
}
/* Replace language stack keys with actual content */
$this->language->parseOutputForDisplay( $this->emailBody['plain'] );
$this->language->parseOutputForDisplay( $this->emailBody['html'] );
$this->language->parseOutputForDisplay( $this->subject );
/* Replace attachment and emoticon URLs */
\IPS\Output::i()->parseFileObjectUrls( $this->emailBody['html'] );
\IPS\Output::i()->parseFileObjectUrls( $this->emailBody['plain'] );
/* Outlook can't handle protocol-relative URLs, so we need to switch them out */
foreach( $this->emailBody as $type => $content )
{
$this->emailBody[ $type ] = preg_replace_callback( "/\s+?src=(['\"])\/\/([^'\"]+?)(['\"])/ims", function( $matches ){
$baseUrl = parse_url( \IPS\Settings::i()->base_url );
/* Try to preserve http vs https */
if( isset( $baseUrl['scheme'] ) )
{
$url = $baseUrl['scheme'] . '://' . $matches[2];
}
else
{
$url = 'http://' . $matches[2];
}
return " src={$matches[1]}{$url}{$matches[1]}";
}, $content );
}
/* Now build the actual email body and send */
$boundary = "--==_mimepart_" . md5( uniqid() );
$this->headers['Content-Type'] = "multipart/alternative; boundary=\"{$boundary}\"; charset=UTF-8";
$this->headers['Content-Transfer-Encoding'] = "8bit";
$this->_buildMessage( $boundary );
/* Send the email - log any errors to the email error log */
try
{
$this->_send();
/* If it was successful, reset the failure count */
\IPS\Data\Store::i()->failedMailCount = 0;
}
catch( \IPS\Email\Outgoing\Exception $e )
{
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => implode( ', ', $this->to ),
'mlog_from' => $this->from,
'mlog_subject' => $this->subject,
'mlog_content' => $this->emailBody['html'] ?: $this->emailBody['plain'],
'mlog_resend_data' => json_encode( array( 'headers' => array_map( function( $header ){ return (string) $header; }, $this->headers ), 'body' => $this->emailBody, 'boundary' => $boundary ) ),
'mlog_msg' => $e->getMessage(),
'mlog_smtp_log' => $this->getLog(),
) );
/* Update or set failure count */
$failedCount = 1;
if( isset( \IPS\Data\Store::i()->failedMailCount ) )
{
$failedCount += \IPS\Data\Store::i()->failedMailCount;
}
\IPS\Data\Store::i()->failedMailCount = $failedCount;
/* Return the exception so it can be used */
return $e;
}
return TRUE;
}
/**
* Merge and Send
*
* @param array $recipients Array where the keys are the email addresses to send to and the values are an array of variables to replace
* @return void
*/
public function mergeAndSend( $recipients )
{
if ( \IPS\Settings::i()->mandrill_api_key && \IPS\Settings::i()->mandrill_use_for == 2 )
{
$toArray = array();
$recipientsMerge = array();
foreach ( $recipients as $address => $_vars )
{
$vars = array();
foreach ( $_vars as $k => $v )
{
$vars[] = array( 'name' => $k, 'content' => $v );
}
$toArray[] = array( 'email' => $address );
$recipientsMerge[] = array( 'rcpt' => $address, 'vars' => $vars );
}
/* Replace language stack keys with actual content */
$body = $this->body['html'];
$subject = $this->subject;
/* Replace attachment and emoticon URLs */
\IPS\Output::i()->parseFileObjectUrls( $body );
$this->language->parseOutputForDisplay( $body );
$this->language->parseOutputForDisplay( $subject );
$response = \IPS\Email::mandrill( 'messages_send', \IPS\Settings::i()->mandrill_api_key, array(
'message' => array(
'html' => $body,
'subject' => $subject,
'from_email' => $this->from,
'from_name' => $this->fromName,
'to' => $toArray,
'auto_text' => TRUE,
'url_strip_qs' => FALSE,
'preserve_recipients' => FALSE,
'merge' => TRUE,
'global_merge_vars' => array(),
'merge_vars' => $recipientsMerge,
'tags' => array_merge( array( 'ips' ) )
),
'async' => TRUE
) );
}
else
{
foreach ( $recipients as $address => $vars )
{
$content = $this->body;
$subject = $this->subject;
/* Replace attachment and emoticon URLs */
\IPS\Output::i()->parseFileObjectUrls( $content['html'] );
\IPS\Output::i()->parseFileObjectUrls( $content['plain'] );
/* Replace language stack keys with actual content */
$this->language->parseOutputForDisplay( $content['html'] );
$this->language->parseOutputForDisplay( $content['plain'] );
$this->language->parseOutputForDisplay( $subject );
foreach ( $vars as $k => $v )
{
$content['html'] = str_replace( "*|{$k}|*", $v, $content['html'] );
$content['plain'] = str_replace( "*|{$k}|*", $v, $content['plain'] );
$subject = str_replace( "*|{$k}|*", $v, $subject );
}
$email = static::buildFromContent( $subject, $content['html'], $content['plain'] );
$email->useWrapper = FALSE;
$email->send( $address );
unset( $email );
}
}
}
/**
* This method allows you to quickly send an email, bypassing the automatic formatting and email error logging.
* This is primarily used by the "resend failed emails". To use this method you must manually populate $this->headers and $this->emailBody.
*
* @param string $boundary The boundary used in the Content-Type header
* @return bool
* @throws \IPS\Email\Outgoing\Exception
*/
public function forceSend( $boundary )
{
$this->_buildMessage( $boundary );
/* Smtp relies on $this->recipients being populated for the RCPT TO command, which is not done when force sending */
if ( \IPS\Settings::i()->mail_method === 'smtp' )
{
$this->_parseRecipients( $this->to, TRUE );
}
$this->_send();
}
/**
* Build the email message
*
* @param string $boundary The boundary used in the Content-Type header
* @return bool
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _buildMessage( $boundary )
{
foreach ( array( 'text/plain' => $this->emailBody['plain'], 'text/html' => $this->emailBody['html'] ) as $contentType => $content )
{
$this->message .= "--{$boundary}{$this->eol}";
$this->message .= "Content-Type: {$contentType}; charset=UTF-8{$this->eol}";
$this->message .= preg_replace( "/(?<!\r)\n/", "{$this->eol}", $content );
$this->message .= "{$this->eol}";
}
$this->message .= "--{$boundary}--{$this->eol}";
}
/**
* Parse Recipients
*
* @param string|array|\IPS\Member $data The member or email address, or array of members or email addresses, to send to
* @param bool $emailOnly If TRUE, will use email only rather than names too. Set to TRUE for the "To" header
* @return string
* @see <a href='http://www.faqs.org/rfcs/rfc2822.html'>RFC 2822</a>
* @throws \InvalidArgumentException
* @li BAD_RECIPIENT
*/
protected function _parseRecipients( $data, $emailOnly=FALSE )
{
$return = array();
if ( !is_array( $data ) )
{
$data = array( $data );
}
foreach ( $data as $recipient )
{
if ( is_string( $recipient ) )
{
$this->to[] = $recipient;
$return[] = $emailOnly ? $recipient : "<{$recipient}>";
}
elseif ( $recipient instanceof \IPS\Member )
{
$this->to[] = $recipient->email;
$return[] = $emailOnly ? $recipient->email : mb_encode_mimeheader($recipient->name) . " <{$recipient->email}>";
$this->language = $recipient->language( TRUE );
}
else
{
throw new \InvalidArgumentException;
}
}
$this->recipients = array_merge( $this->recipients, $return );
return implode( ', ', $return );
}
/**
* IN_DEV - load and run template
*
* @param string $functionName Function name to use
* @param string $templateContents Content to parse
* @param array $params Params
* @return string
*/
protected static function devProcessTemplate( $functionName, $templateContents, $params )
{
if( !function_exists( 'IPS\\Theme\\' . $functionName ) )
{
preg_match( '/^<ips:template parameters="(.+?)?" \/>(\r\n?|\n)/', $templateContents, $matches );
if ( isset( $matches[0] ) )
{
static::$matchesStore = isset( $matches[1] ) ? $matches[1] : '';
$templateContents = preg_replace( '/^<ips:template parameters="(.+?)?" \/>(\r\n?|\n)/', '', $templateContents );
}
else
{
/* Subjects do not contain the ips:template header, so we need a little magic */
if ( $params !== NULL and is_array( $params ) and count( $params ) )
{
/* Extract app and key from "email__{app}_{key}_subject" */
list( $app, $key ) = explode( '_', mb_substr( $functionName, 7, -8 ), 2 );
if ( $app and $key )
{
/* Doesn't matter if it's HTML or TXT here, we just want the param list */
$key = md5( $app . ';' . $key ) . '_email_html';
$template = isset( \IPS\Data\Store::i()->$key ) ? \IPS\Data\Store::i()->$key : NULL;
if ( $template )
{
preg_match( "#function\s+?([^\(]+?)\(([^\)]+?)\)#", $template, $matches );
if ( isset( $matches[2] ) )
{
static::$matchesStore = trim( $matches[2] );
}
}
else
{
/* Grab the param list from the database */
try
{
$template = \IPS\Db::i()->select( 'template_name, template_data', 'core_email_templates', array( 'template_app=? AND template_name=?', $app, $key ) )->first();
if ( isset( $template['template_name'] ) )
{
static::$matchesStore = $template['template_data'];
}
}
catch( \UnderflowException $e )
{
if ( \IPS\IN_DEV )
{
/* Try and get template file */
list( $app, $key ) = explode( '_', mb_substr( $functionName, 7, -8 ), 2 );
foreach( array( 'phtml', 'txt' ) AS $type )
{
/* We only need one */
if ( $file = @file_get_contents( \IPS\ROOT_PATH . "/applications/{$app}/dev/email/{$key}.{$type}" ) )
{
break;
}
}
if ( $file !== FALSE )
{
preg_match( '/^<ips:template parameters="(.+?)?" \/>(\r\n?|\n)/', $file, $matches );
static::$matchesStore = isset( $matches[1] ) ? $matches[1] : '';
}
else
{
throw new \BadMethodCallException( 'NO_EMAIL_TEMPLATE_FILE - ' . $app . '/' . $key . '.' . $type );
}
}
else
{
/* I can't really help you, sorry */
throw new \LogicException;
}
}
}
}
}
}
\IPS\Theme::makeProcessFunction( $templateContents, $functionName, static::$matchesStore );
}
return call_user_func_array( 'IPS\\Theme\\'.$functionName, $params );
}
/**
* Return any additional log data
*
* @return string
*/
public function getLog()
{
return '';
}
/**
* Send a Mandrill Request
*
* @param string $method Method
* @param string $apiKey API Key
* @param array $args Arguments
* @return array
* @thows \IPS\Http\Request\Exception
* @note Sending a lot of users to Mandrill can take longer than the default 5 second timeout, so we set it to 20 second timeout
*/
public static function mandrill( $method, $apiKey, $args=array() )
{
return \IPS\Http\Url::external( 'https://mandrillapp.com/api/1.0/' . str_replace( '_', '/', $method ) . '.json' )
->request( 20 )
->setHeaders( array( 'Content-Type' => 'application/json' ) )
->post( json_encode( array_merge( array( 'key' => $apiKey ), $args ) ) )
->decodeJson( FALSE );
}
/**
* Makes HTML acceptable for use in emails
*
* @param string $text The text
* @return string
*/
public function parseTextForEmail( $text )
{
\IPS\Output::i()->parseFileObjectUrls( $text );
try
{
$document = new \DomDocument( '1.0', 'UTF-8' );
$document->loadHTML( "<head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'/></head>" . $text );
$this->_parseNodeForEmail( $document );
return preg_replace( '/^<!DOCTYPE.+?>/', '', str_replace( array( '<html>', '</html>', '<head>', '</head>', '<body>', '</body>', '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' ), '', $document->saveHTML() ) );
}
catch( \Exception $e )
{
return $text;
}
}
/**
* Makes HTML acceptable for use in emails
*
* @param string $text The text
* @return string
*/
public function _parseNodeForEmail( \DOMNode &$node )
{
if ( $node->hasChildNodes() )
{
/* Dom node lists are "live" and if you modify the tree, you may affect the index which also affects php foreach loops. Subsequently we
need to capture all the nodes in a loop and store them, and then loop over that store */
$_nodes = array();
foreach ( $node->childNodes as $child )
{
$_nodes[] = $child;
}
foreach( $_nodes as $_node )
{
$this->_parseNodeForEmail( $_node );
}
}
if ( $node instanceof \DOMElement )
{
$parent = $node->parentNode;
/* Outlook cannot handle protocol-relative URLs like //site.com/image.jpg - it hangs the client and freezes */
if ( $node->tagName == 'img' )
{
if ( $node->getAttribute('src') )
{
if ( mb_substr( $node->getAttribute('src'), 0, 2 ) === '//' )
{
$baseUrl = parse_url( \IPS\Settings::i()->base_url );
/* Try to preserve http vs https */
if( isset( $baseUrl['scheme'] ) )
{
$url = $baseUrl['scheme'] . ':' . $node->getAttribute('src');
}
else
{
$url = 'http:' . $node->getAttribute('src');
}
$node->setAttribute( 'src', $url );
}
}
}
if ( $node->getAttribute('class') )
{
foreach ( explode( ' ', $node->getAttribute('class') ) as $class )
{
switch ( $class )
{
/* Make quotes look good */
case 'ipsQuote':
$cell = $this->_createContainerTable( $parent, $node );
$cell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; margin: 0;border: 1px solid #e0e0e0;border-left: 3px solid #adadad;position: relative;font-size: 13px;background: #fdfdfd" );
$citation = $this->_createContainerTable( $cell );
$citation->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; background: #f5f5f5;padding: 8px 15px;color: #000;font-weight: bold;font-size: 13px;display: block;" );
$citation->appendChild( new \DOMText( $node->getAttribute('data-cite') ) );
$containerCell = $this->_createContainerTable( $cell );
$containerCell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; padding-left:15px" );
while( $node->childNodes->length )
{
foreach ( $node->childNodes as $child )
{
$containerCell->appendChild( $child );
}
}
$parent->removeChild( $node );
break;
/* Make code look good */
case 'ipsCode':
$cell = $this->_createContainerTable( $parent, $node );
$cell->setAttribute( 'style', "font-family: monospace; line-height: 1.5; font-size: 14px; background: #fafafa; padding: 0; border-left: 4px solid #e0e0e0;" );
$p = new \DOMElement( 'pre' );
$cell->appendChild( $p );
$p->setAttribute( 'style', "font-family: monospace; line-height: 1.5; font-size: 14px; padding-left:15px" );
while( $node->childNodes->length )
{
foreach ( $node->childNodes as $child )
{
$p->appendChild( $child );
}
}
$parent->removeChild( $node );
break;
/* Remove spoilers */
case 'ipsStyle_spoiler':
$cell = $this->_createContainerTable( $parent, $node );
$cell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; margin: 0;padding: 10px;background: #363636;color: #d8d8d8;" );
$cell->appendChild( new \DOMText( $this->language->addToStack('email_spoiler_line') ) );
$parent->removeChild( $node );
break;
/* Remove Videos */
case 'ipsEmbeddedVideo':
$cell = $this->_createContainerTable( $parent, $node );
$cell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px; padding: 10px; margin: 0;border: 1px solid #e0e0e0;border-left: 3px solid #adadad;position: relative;font-size: 13px;background: #fdfdfd" );
$cell->appendChild( new \DOMText( $this->language->addToStack('email_video_line') ) );
$parent->removeChild( $node );
break;
/* Constrain images */
case 'ipsImage':
$maxImageDims = \IPS\Settings::i()->attachment_image_size ? explode( 'x', \IPS\Settings::i()->attachment_image_size ) : array( 1000, 750 );
$node->setAttribute( 'style', "max-width:{$maxImageDims[0]}px;max-height:{$maxImageDims[1]}px;" );
break;
}
}
}
/* Email clients do not like iframes. */
if ( $node->tagName == 'iframe' )
{
if ( $node->getAttribute('src') )
{
$url = new \IPS\Http\Url( $node->getAttribute('src') );
/* Strip "do" param, but only if it is set to "embed" */
if ( isset( $url->queryString['do'] ) AND $url->queryString['do'] == 'embed' )
{
$url = $url->stripQueryString( 'do' );
}
$a = new \DOMElement( 'a' );
$parent->insertBefore( $a, $node );
$a->setAttribute( 'href', (string) $url );
$a->appendChild( new \DOMText( (string) $url ) );
$parent->removeChild( $node );
}
}
}
}
/**
* Create container table as some email clients can't handle things if they're not in tables
*
* @param \DOMNode $node The node to put the table into
* @param \DOMNode|null $replace If the table should replace an existing node, the node to be replaced
* @return \DOMNode
*/
protected function _createContainerTable( $node, $replace=NULL )
{
$table = new \DOMElement( 'table' );
$row = new \DOMElement( 'tr' );
$cell = new \DOMElement( 'td' );
if ( $replace )
{
$node->insertBefore( $table, $replace );
}
else
{
$node->appendChild( $table );
}
$table->appendChild( $row );
$row->appendChild( $cell );
$table->setAttribute( 'width', '100%' );
$table->setAttribute( 'cellpadding', '0' );
$table->setAttribute( 'cellspacing', '0' );
$table->setAttribute( 'border', '0' );
$cell->setAttribute( 'style', "font-family: 'Helvetica Neue', helvetica, sans-serif; line-height: 1.5; font-size: 14px;" );
return $cell;
}
}