Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+383 -120
View File
@@ -11,7 +11,7 @@
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -21,48 +21,52 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
* Outgoing Email
*
* An object of this class represents an outgoing email preparing to be sent. An object is constructed either by:
* $email = \IPS\Email::buildFromTemplate( ... )
* `$email = \IPS\Email::buildFromTemplate( ... )`
* Or, less frequently:
* $email = \IPS\Email::buildFromContent( ... )
* `$email = \IPS\Email::buildFromContent( ... )`
*
* At the time of construction, no parsing (such as language parsing) is done. That is only done when the email is sent:
* $email->send( $member );
* `$email->send( $member );`
* It is acceptable to construct a single object and send to multiple members:
* $email->send( $member1 );
* $email->send( $member2 );
* Because no parsing is done until send() is called, these two members do not even need to be using the same language.
* `$email->send( $member1 );`
* `$email->send( $member2 );`
* Because no parsing is done until `send()` is called, these two members do not even need to be using the same language.
*
* Although this is possible, if sending an email to lots of members, the mergeAndSend() method can be used, which assumes
* tags in the {{tag}} format will be used, and provides a way to do a merge send. Though for most outgoing email methods
* there is essentially no benefit to do this versus lots of ->send() calls, if using a service like SparkPost, a single
* API call will be used, which is of course much more efficient. Unlike send(), mergeAndSend() requires each recipient
* Although this is possible, if sending an email to lots of members, the `mergeAndSend()` method can be used, which assumes
* tags in the `{{tag}}` format will be used, and provides a way to do a merge send. Though for most outgoing email methods
* there is essentially no benefit to do this versus lots of `->send()` calls, if using a service like SparkPost, a single
* API call will be used, which is of course much more efficient. Unlike `send()`, `mergeAndSend()` requires each recipient
* to be expecting the same language
* $email->mergeAndSend( [ 'user1@example.com' => [ 'member_name' => "User 1" ], 'user2@example.com' => [ 'member_name' => "User 2" ] ], $language );
* `$email->mergeAndSend( [ 'user1@example.com' => [ 'member_name' => "User 1" ], 'user2@example.com' => [ 'member_name' => "User 2" ] ], $language );`
*
* A handy method for debugging is to output the compiled content:
* echo $email->compileContent( 'html', $member );
* `echo $email->compileContent( 'html', $member );`
*/
abstract class _Email
{
const TYPE_TRANSACTIONAL = 'transactional'; // A single recipient messages that is used operationally, usually in response to a specific action. For example, to reset a password.
const TYPE_LIST = 'list'; // A notification about something in particular that the user has opted into, but may be sent to multiple users who have also opted to receive notifications for the same thing.
const TYPE_BULK = 'bulk'; // A bulk mail sent to multiple recipients that is not in response to something the user has opted in to.
/* !Configuration */
/**
* @brief Transaction email (constant)
* @note A single recipient messages that is used operationally, usually in response to a specific action. For example, to reset a password.
*/
const TYPE_TRANSACTIONAL = 'transactional';
/**
* @brief Transaction email (constant)
* @note A notification about something in particular that the user has opted into, but may be sent to multiple users who have also opted to receive notifications for the same thing.
*/
const TYPE_LIST = 'list';
/**
* @brief Transaction email (constant)
* @note A bulk mail sent to multiple recipients that is not in response to something the user has opted in to.
*/
const TYPE_BULK = 'bulk';
/**
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = \IPS\BULK_MAILS_PER_CYCLE;
/**
* @brief If sending a bulk email to more than MAX_EMAILS_PER_GO - does this
* class require waiting between cycles? For "standard" classes like
* PHP and SMTP, this will be TRUE - and will cause bulk mails to go
* to a class. For APIs like SparkPost, this can be FALSE
*/
const REQUIRES_TIME_BREAK = TRUE;
/* !Factory Constructors */
/**
@@ -73,7 +77,7 @@ abstract class _Email
*/
public static function classToUse( $type )
{
if( defined( '\IPS\EMAIL_DEBUG_PATH' ) AND \IPS\EMAIL_DEBUG_PATH )
if( \defined( '\IPS\EMAIL_DEBUG_PATH' ) AND \IPS\EMAIL_DEBUG_PATH )
{
return 'IPS\Email\Outgoing\Debug';
}
@@ -140,10 +144,12 @@ abstract class _Email
* @param string|NULL $plaintextContent Plaintext version. If not provided, one will be built automatically based off $htmlContent.
* @param string $type See TYPE_* constants. While it defaults to "transactional", this is to maintain backwards compatibility and a type should always be specified.
* @param bool $useWrapper If TRUE, the email will be wrapped in the default wrapper template
* @param string $emailKey Key used to identify the email, used for tracking purposes
* @param bool $trackingEnabled TRUE by default, pass FALSE to explicitly disable log/view tracking. Useful with mergeAndSend()'s default behavior of rebuilding an already built email
* @return \IPS\Email
* @deprecated Not specifying an argument for $type is deprecated
*/
public static function buildFromContent( $subject, $htmlContent='', $plaintextContent=NULL, $type = 'transactional', $useWrapper=TRUE )
public static function buildFromContent( $subject, $htmlContent='', $plaintextContent=NULL, $type = 'transactional', $useWrapper=TRUE, $emailKey=NULL, $trackingEnabled=TRUE )
{
$email = static::factory( $type );
$email->type = $type;
@@ -151,6 +157,13 @@ abstract class _Email
$email->htmlContent = $htmlContent;
$email->plaintextContent = ( $plaintextContent === NULL ) ? static::buildPlaintextBody( $htmlContent ) : $plaintextContent;
$email->useWrapper = $useWrapper;
$email->templateKey = $emailKey;
if( $trackingEnabled === FALSE )
{
$email->trackingCompleted = array( 'html' => TRUE, 'plaintext' => TRUE );
}
return $email;
}
@@ -223,7 +236,7 @@ abstract class _Email
*
* @param string $app App name
* @param string $template Template name
* @param array $$parameters Parameters
* @param array $parameters Parameters
* @return \IPS\Email
*/
public function setUnsubscribe( $app, $template, $parameters = array() )
@@ -233,6 +246,48 @@ abstract class _Email
$this->unsubscribeParams = $parameters;
return $this;
}
/**
* @brief Container information used for restricting ads
*/
protected $advertisementParams = NULL;
/**
* Specify a specific container to attempt to load an advertisement from
*
* @param string $className Node class to restrict to
* @param int $id Node id to restrict to
* @return \IPS\Email
*/
public function setAdvertisementParameters( $className, $id )
{
$this->advertisementParams = array( 'className' => $className, 'id' => $id );
return $this;
}
/**
* Return the advertisement HTML to embed into an email
*
* @param string $type html (default) or plaintext
* @return string
*/
public function getAdvertisement( $type='html' )
{
if( $advertisement = \IPS\core\Advertisement::loadForEmail( $this->advertisementParams ) )
{
return $advertisement->toString( $type, $this );
}
else
{
return '';
}
}
/**
* @brief Flag if we have already added tracking tokens so we do not do it again
*/
public $trackingCompleted = array( 'html' => FALSE, 'plaintext' => FALSE );
/**
* Compile the content which will actually be sent
@@ -240,9 +295,10 @@ abstract class _Email
* @param string $type 'html' or 'plaintext'
* @param \IPS\Member|NULL|FALSE $member If the email is going to a member, the member object. Ensures correct language is used and the email starts with "Hi {member}". NULL for no member, FALSE to use "Hi *|member_name|*" for mergeAndSend()
* @param \IPS\Lang|NULL $language If provided, will override the $member language
* @param bool $logViews If set to FALSE, will skip logging the email and including the view pixel
* @return string
*/
public function compileContent( $type, $member = NULL, \IPS\Lang $language = NULL )
public function compileContent( $type, $member = NULL, \IPS\Lang $language = NULL, $logViews = TRUE )
{
/* Setting $language as a property is a bit confusing because the email doesn't *have* a language - it could
change for different recipients, but since the templates expect it as a property we set it here for
@@ -286,16 +342,116 @@ abstract class _Email
/* Wrap */
$return = static::template( 'core', 'emailWrapper', $type, array( $this->subject, $member ?: new \IPS\Member, $return, $unsubscribe, $member === FALSE, '', $this, $ourPicks ) );
}
else
{
$return = static::template( 'core', 'emailNoWrapper', $type, array( $this->subject, $member ?: new \IPS\Member, $return, '', $member === FALSE, '', $this, NULL ) );
}
/* Parse language */
$language->parseEmail( $return );
/* Parse URLs */
static::parseFileObjectUrls( $return );
/* Add the view tracking pixel and click tracking if appropriate */
if( $logViews === TRUE AND $this->trackingCompleted[ $type ] !== TRUE )
{
if( \IPS\Settings::i()->prune_log_emailstats != 0 )
{
/* Click tracking */
if( $type == 'plaintext' )
{
static::addPlaintextClickTracking( $return );
}
else
{
static::addHtmlClickTracking( $return );
}
}
if( \count( \IPS\core\Advertisement::$advertisementIdsEmail ) )
{
$imageUrl = (string) \IPS\Http\Url::external( rtrim( \IPS\Settings::i()->base_url, '/' ) . '/applications/core/interface/email/views.php' )->setQueryString( 'ads', implode( ',', \IPS\core\Advertisement::$advertisementIdsEmail ) );
$return = str_replace( '</body>', "<img width='1' height='1' src='{$imageUrl}' border='0'></body>", $return );
}
$this->trackingCompleted[ $type ] = TRUE;
}
/* Return */
return $return;
}
/**
* Add click tracking to plain text emails
*
* @param string $content The email content
* @return void
*/
protected function addPlaintextClickTracking( &$content )
{
$content = preg_replace_callback( '#(?:^|\s|\)|\(|\{|\}|/>|>|\]|\[|;|href=\S)((http|https|news|ftp)://(?:[^<>\)\[\"\s]+|[a-zA-Z0-9/\._\-!&\#;,%\+\?:=]+))#is', function ($matches) {
$url = \IPS\Http\Url::internal( "app=core&module=system&controller=redirect", 'front' )->setQueryString( array(
'url' => trim( $matches[0] ),
'key' => hash_hmac( "sha256", trim( $matches[0] ), \IPS\Settings::i()->site_secret_key . 'r' ),
'email' => 1,
'type' => $this->templateKey
) );
return (string) $url;
}, $content );
}
/**
* Add click tracking to HTML emails
*
* @param string $content The email content
* @return void
*/
protected function addHtmlClickTracking( &$content )
{
$document = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' );
$document->loadHTML( $content );
/* Get document links */
$links = $document->getElementsByTagName( 'a' );
foreach( $links as $element )
{
static::_parseElementForClickTracking( $element, $this->templateKey );
}
/* DOMDocument will change href="*|unfollow_link" to href="*%7Cunfollow_link%7C*", so we need to change it back
so mergeAndSend() can do its thing properly */
$content = preg_replace( "/\*%7C([a-z0-9_]+)%7C\*/ims", "*|$1|*", $document->saveHTML() );
}
/**
* Parse a DOM Element to add click tracking to a link
*
* @param \DOMElement $element Anchor tag element
* @param string|null $templateKey The key for the template, used for logging/tracking
* @return void
*/
protected static function _parseElementForClickTracking( $element, $templateKey=NULL )
{
/* If we are working with mergeAndSend() this may not be a full URL (which may be passed as a user param)
so we need to ignore any exceptions and simply not adjust the link, which should be done manually instead */
try
{
$url = \IPS\Http\Url::internal( "app=core&module=system&controller=redirect", 'front' )->setQueryString( array(
'url' => (string) $element->getAttribute('href'),
'resource' => ( \IPS\Request::i()->resource ) ? 1 : NULL,
'key' => hash_hmac( "sha256", (string) $element->getAttribute('href'), \IPS\Settings::i()->site_secret_key . 'r' ),
'email' => 1,
'type' => $templateKey
) );
$element->setAttribute( 'href', (string) $url );
}
catch( \IPS\Http\Url\Exception $e ){}
}
/**
* Get subject
@@ -325,7 +481,12 @@ abstract class _Email
/* Using a template */
elseif ( $this->templateApp )
{
$return = trim( static::devProcessTemplate( "email__{$this->templateApp}_{$this->templateKey}_subject_" . str_replace( array( ' ', '.', '-', '@' ), '_', $language->short ), $language->get( "mailsub__{$this->templateApp}_{$this->templateKey}" ), array_merge( $this->templateParams, array( $this ) ) ) );
$return = trim( static::devProcessTemplate(
"email__{$this->templateApp}_{$this->templateKey}_subject_" . str_replace( array( ' ', '.', '-', '@' ), '_', $language->short ),
$language->get( "mailsub__{$this->templateApp}_{$this->templateKey}" ),
array_merge( $this->templateParams, array( $this ) ),
'plaintext'
) );
}
/* Parse language */
@@ -499,9 +660,10 @@ abstract class _Email
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send
* @param boolean $autoSubmitted The email was auto-generated (yes for notification, bulk mail, no for contact us form)
* @param boolean $updateAds TRUE to update ad impression count (by one), FALSE to skip (used for mergeAndSend where one ad is used many times)
* @return bool
*/
public function send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), $autoSubmitted = TRUE )
public function send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), $autoSubmitted = TRUE, $updateAds = TRUE )
{
/* Check we have recipients */
if ( !static::_parseRecipients( $to, TRUE ) )
@@ -515,12 +677,18 @@ abstract class _Email
$this->autoSubmitted = $autoSubmitted;
/* Send */
$this->_send( $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders);
/* If it was successful, reset the failure count */
if ( !isset( \IPS\Data\Store::i()->failedMailCount ) OR \IPS\Data\Store::i()->failedMailCount >= 1 )
$this->_send( $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders );
/* Log the send if enabled */
$this->_trackStatistics();
/* Sent successfully, remove notification */
\IPS\core\AdminNotification::remove( 'core', 'ConfigurationError', 'failedMail' );
/* Update the ad impression count if appropriate */
if( $updateAds === TRUE )
{
\IPS\Data\Store::i()->failedMailCount = 0;
\IPS\core\Advertisement::updateEmailImpressions();
}
/* Return */
@@ -537,27 +705,63 @@ abstract class _Email
$boundary = "--==_mimepart_" . md5( mt_rand() );
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => static::_parseRecipients( $to, TRUE ),
'mlog_from' => $fromEmail,
'mlog_subject' => $subject,
'mlog_content' => $html ?: $plaintext,
'mlog_resend_data' => json_encode( array( 'type' => $this->type, 'headers' => $this->_compileHeaders( $subject, $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders, $boundary ), 'body' => array( 'html' => $html, 'plain' => $plaintext ), 'boundary' => $boundary ) ),
'mlog_msg' => json_encode( array( 'message' => $e->getMessage(), 'details' => $e->extraDetails ) ),
'mlog_smtp_log' => $this->getLog(),
'mlog_date' => time(),
'mlog_to' => static::_parseRecipients( $to, TRUE ),
'mlog_from' => $fromEmail,
'mlog_subject' => $subject,
'mlog_content' => $html ?: $plaintext,
'mlog_resend_data' => json_encode( array( 'type' => $this->type, 'headers' => $this->_compileHeaders( $subject, $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders, $boundary ), 'body' => array( 'html' => $html, 'plain' => $plaintext ), 'boundary' => $boundary ) ),
'mlog_msg' => json_encode( array( 'message' => $e->getMessage(), 'details' => $e->extraDetails ) ),
'mlog_smtp_log' => $this->getLog(),
'mlog_notification_sent' => FALSE
) );
/* 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 */
return FALSE;
}
/* Catch any parse errors occurred when compiling an email */
catch( \ParseError $e )
{
\IPS\Log::log( $e, 'email_compile_failed' );
return FALSE;
}
}
/**
* Track the number of emails sent
*
* @param int $number Number of emails being sent
* @return void
*/
protected function _trackStatistics( $number=1 )
{
if( \IPS\Settings::i()->prune_log_emailstats == 0 )
{
return;
}
/* If we have a row for "today" then update it, otherwise insert one */
$today = \IPS\DateTime::create()->format( 'Y-m-d', $this->language );
try
{
/* We only include the time column in the query so that the db index can be effectively used */
if( $this->templateKey === NULL )
{
$currentRow = \IPS\Db::i()->select( '*', 'core_statistics', array( 'type=? AND time>? AND value_4=? AND extra_data IS NULL', 'emails_sent', 1, $today ) )->first();
}
else
{
$currentRow = \IPS\Db::i()->select( '*', 'core_statistics', array( 'type=? AND time>? AND value_4=? AND extra_data=?', 'emails_sent', 1, $today, $this->templateKey ) )->first();
}
\IPS\Db::i()->update( 'core_statistics', "value_1=value_1+{$number}", array( 'id=?', $currentRow['id'] ) );
}
catch( \UnderflowException $e )
{
\IPS\Db::i()->insert( 'core_statistics', array( 'type' => 'emails_sent', 'value_1' => $number, 'value_4' => $today, 'time' => time(), 'extra_data' => $this->templateKey ) );
}
}
/**
@@ -602,9 +806,15 @@ abstract class _Email
$currentLocale = setlocale( LC_ALL, '0' );
$language->setLocale();
/* We need to know before we start if tracking has been completed or not for the content already */
$trackingCompleted = $this->trackingCompleted;
foreach ( $recipients as $address => $vars )
{
$member = \IPS\Member::load( $address, 'email' );
/* Before compiling our content, reset the "tracking completed flag", otherwise if it hasn't been done yet, the flag is set during the first loop and never reset (so tracking isn't performed) for subsequent loops */
$this->trackingCompleted = $trackingCompleted;
$subject = $this->compileSubject( $member, $language );
$htmlContent = $this->compileContent( 'html', $member, $language );
$plaintextContent = $this->compileContent( 'plaintext', $member, $language );
@@ -614,7 +824,7 @@ abstract class _Email
{
$language->parseEmail( $v );
$htmlContent = str_replace( "*|{$k}|*", $v, $htmlContent );
$htmlContent = str_replace( "*|{$k}|*", htmlspecialchars( $v, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', TRUE ), $htmlContent );
$plaintextContent = str_replace( "*|{$k}|*", $v, $plaintextContent );
$subject = str_replace( "*|{$k}|*", $v, $subject );
@@ -623,13 +833,16 @@ abstract class _Email
$_additionalHeaders[ $headerKey ] = str_replace( "*|{$k}|*", $v, $headerValue );
}
}
if ( static::buildFromContent( $subject, $htmlContent, $plaintextContent, $this->type, FALSE )->send( $address, array(), array(), $fromEmail, $fromName, $_additionalHeaders ) )
if ( static::buildFromContent( $subject, $htmlContent, $plaintextContent, $this->type, FALSE, $this->templateKey, FALSE )->send( $address, array(), array(), $fromEmail, $fromName, $_additionalHeaders, TRUE, FALSE ) )
{
$return++;
}
}
/* Update ad impression count */
\IPS\core\Advertisement::updateEmailImpressions( $return );
/* Now restore the locale we started with */
\IPS\Lang::restoreLocale( $currentLocale );
@@ -662,7 +875,7 @@ abstract class _Email
$file = \IPS\ROOT_PATH . "/applications/{$app}/dev/email/{$template}.{$extension}";
}
return static::devProcessTemplate( "email_{$type}_{$app}_{$template}", file_get_contents( $file ), $params );
return static::devProcessTemplate( "email_{$type}_{$app}_{$template}", file_get_contents( $file ), $params, $type );
}
else
{
@@ -671,16 +884,16 @@ abstract class _Email
if ( !isset( \IPS\Data\Store::i()->$key ) )
{
$templateData = \IPS\Db::i()->select( '*', 'core_email_templates', array( "template_app=? AND template_name=?", $app, $template ), 'template_parent DESC' )->first();
\IPS\Data\Store::i()->$key = "namespace IPS\Theme;\n" . \IPS\Theme::compileTemplate( $templateData['template_content_html'], "email_html_{$app}_{$template}", $templateData['template_data'] ) . "\n" . \IPS\Theme::compileTemplate( $templateData['template_content_plaintext'], "email_plaintext_{$app}_{$template}", $templateData['template_data'] );
\IPS\Data\Store::i()->$key = "namespace IPS\Theme;\n" . \IPS\Theme::compileTemplate( $templateData['template_content_html'], "email_html_{$app}_{$template}", $templateData['template_data'], $type === 'html' ) . "\n" . \IPS\Theme::compileTemplate( $templateData['template_content_plaintext'], "email_plaintext_{$app}_{$template}", $templateData['template_data'], $type === 'html' );
}
$functionName = "IPS\\Theme\\email_{$type}_{$app}_{$template}";
if( !function_exists( $functionName ) )
if( !\function_exists( $functionName ) )
{
eval( \IPS\Data\Store::i()->$key );
}
return call_user_func_array( $functionName, $params );
return $functionName( ...$params );
}
}
@@ -695,11 +908,12 @@ abstract class _Email
* @param string $functionName Function name to use
* @param string $templateContents Content to parse
* @param array $params Params
* @param string $type 'html' or 'plaintext'
* @return string
*/
protected static function devProcessTemplate( $functionName, $templateContents, $params )
protected static function devProcessTemplate( $functionName, $templateContents, $params, $type )
{
if( !function_exists( 'IPS\\Theme\\' . $functionName ) )
if( !\function_exists( 'IPS\\Theme\\' . $functionName ) )
{
preg_match( '/^<ips:template parameters="(.+?)?" \/>(\r\n?|\n)/', $templateContents, $matches );
if ( isset( $matches[0] ) )
@@ -710,7 +924,7 @@ abstract class _Email
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 ) )
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, -( mb_strlen( $functionName ) - mb_strpos($functionName, '_subject' ) ) ), 2 );
@@ -723,7 +937,7 @@ abstract class _Email
if ( $template )
{
preg_match( "#function\s+?([^\(]+?)\(([^\)]+?)\)#", $template, $matches );
preg_match( "#function\s+?([^\(]+?)\((.+?)\)\s*?\{#", $template, $matches );
if ( isset( $matches[2] ) )
{
@@ -732,41 +946,41 @@ abstract class _Email
}
else
{
/* Grab the param list from the database */
try
if ( \IPS\IN_DEV )
{
$template = \IPS\Db::i()->select( 'template_name, template_data', 'core_email_templates', array( 'template_app=? AND template_name=?', $app, $key ), 'template_parent DESC' )->first();
if ( isset( $template['template_name'] ) )
/* Try and get template file */
foreach( array( 'phtml', 'txt' ) AS $type )
{
static::$matchesStore = $template['template_data'];
/* We only need one */
if ( $file = @file_get_contents( \IPS\ROOT_PATH . "/applications/{$app}/dev/email/{$key}.{$type}" ) )
{
break;
}
}
}
catch( \UnderflowException $e )
{
if ( \IPS\IN_DEV )
if ( $file !== FALSE )
{
/* Try and get template file */
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 );
}
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
{
/* 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 ), 'template_parent DESC' )->first();
if ( isset( $template['template_name'] ) )
{
static::$matchesStore = $template['template_data'];
}
}
catch( \UnderflowException $e )
{
/* I can't really help you, sorry */
throw new \LogicException;
@@ -777,10 +991,11 @@ abstract class _Email
}
}
\IPS\Theme::makeProcessFunction( $templateContents, $functionName, static::$matchesStore );
\IPS\Theme::makeProcessFunction( $templateContents, $functionName, static::$matchesStore, $type === 'html' );
}
return call_user_func_array( 'IPS\\Theme\\'.$functionName, $params );
$function = 'IPS\\Theme\\'.$functionName;
return $function( ...$params );
}
/**
@@ -894,7 +1109,7 @@ abstract class _Email
{
$return = array();
if ( !is_array( $data ) )
if ( !\is_array( $data ) )
{
$data = array( $data );
}
@@ -922,7 +1137,7 @@ abstract class _Email
*/
protected function _getMemberFromRecipients( $data )
{
if ( is_array( $data ) )
if ( \is_array( $data ) )
{
$data = array_shift( $data );
}
@@ -973,23 +1188,34 @@ abstract class _Email
*
* @param string $text The text
* @param \IPS\Lang $language Language
* @param null|int $truncate NULL to not truncate, (int) to truncate to (int) chars
* @return string
*/
public static function staticParseTextForEmail( $text, \IPS\Lang $language )
public static function staticParseTextForEmail( $text, \IPS\Lang $language, $truncate=NULL )
{
static::parseFileObjectUrls( $text );
try
if ( $truncate !== NULL and \is_int( $truncate ) and $truncate > 0 )
{
$document = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' );
$document->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $text ) );
static::_parseNodeForEmail( $document, $language );
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 \IPS\Text\Parser::truncate( $text, TRUE, $truncate );
}
else
{
return $text;
try
{
/* Swap out lazy load stuff */
$text = \IPS\Text\Parser::removeLazyLoad( $text );
$document = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' );
$document->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $text ) );
static::_parseNodeForEmail( $document, $language );
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;
}
}
}
@@ -998,23 +1224,24 @@ abstract class _Email
*
* @param string $text The text
* @param \IPS\Lang $language Language. If not provided, will use whatever is set in $this->language - provided for backwards compatibility with templates not sending one
* @param null|int $truncate NULL to not truncate, (int) to truncate to (int) chars
* @return string
*/
public function parseTextForEmail( $text, \IPS\Lang $language = NULL )
public function parseTextForEmail( $text, \IPS\Lang $language = NULL, $truncate=NULL )
{
if ( $language === NULL )
{
$language = $this->language;
}
return static::staticParseTextForEmail( $text, $language );
return static::staticParseTextForEmail( $text, $language, $truncate );
}
/**
* Makes HTML acceptable for use in emails
*
* @param string $text The text
* @param \IPS\Lang $language Language
* @param \DOMElement $node The DOM element
* @param \IPS\Lang $language Language
* @return string
*/
protected static function _parseNodeForEmail( \DOMNode &$node, \IPS\Lang $language )
@@ -1045,8 +1272,8 @@ abstract class _Email
/**
* Makes HTML acceptable for use in emails: Parse Element
*
* @param string $text The text
* @param \IPS\Lang $language Language
* @param \DOMElement $node The DOM element
* @param \IPS\Lang $language Language
* @return string
*/
protected static function _parseElementForEmail( \DOMElement &$node, \IPS\Lang $language )
@@ -1275,11 +1502,16 @@ abstract class _Email
}
}
/* Swap the iframe for a link */
/* Create a link, and a paragraph, insert the paragraph into the document, then the link into the paragraph */
$a = new \DOMElement( 'a' );
$parent->insertBefore( $a, $node );
$p = new \DOMElement( 'p' );
$parent->insertBefore( $p, $node );
$p->appendChild( $a );
$a->setAttribute( 'href', (string) $url );
$a->appendChild( new \DOMText( (string) $url ) );
$parent->removeChild( $node );
}
}
@@ -1323,7 +1555,7 @@ abstract class _Email
/**
* Build an unsubscribe link
*
* @deprecared Use setUnsubscribe() instead
* @deprecated Use setUnsubscribe() instead
* @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
@@ -1345,4 +1577,35 @@ abstract class _Email
return $this;
}
/**
* @brief Store database counts
*/
protected static $_failedMail = [];
/**
* Get number of failed emails
*
* @param \IPS\DateTime|NULL $cutoff
* @param bool $cache Whether to use a cached value
* @param bool $includeNotified Include logs that have already triggered a notification
* @return int
*/
public static function countFailedMail( \IPS\DateTime $cutoff=NULL, bool $cache=TRUE, bool $includeNotified=FALSE ): int
{
$key = $cutoff ? $cutoff->getTimestamp() : 'all';
if( isset( static::$_failedMail[ $key ] ) AND $cache )
{
return static::$_failedMail[ $key ];
}
$where = [ [ 'mlog_notification_sent=?', $includeNotified ] ];
if( $cutoff )
{
$where[] = [ 'mlog_date>?', $cutoff->getTimestamp() ];
}
static::$_failedMail[ $key ] = \IPS\Db::i()->select( 'count(mlog_id)', 'core_mail_error_logs', $where )->first();
return static::$_failedMail[ $key ];
}
}