Version 4.1.19

This commit is contained in:
Neo committed 2025-12-19 05:38:56 -08:00
1 parent 28bd025b35
commit 2bd5025018
2674 files changed
+233518 -77766

No files matched your search

+84 -265
View File
@@ -2,9 +2,9 @@
/**
* @brief Bulkmail central library
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 25 Jun 2013
* @version SVN_VERSION_NUMBER
*/
@@ -37,62 +37,12 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
* @brief Multiton Store
*/
protected static $multitons;
/**
* @brief The number of bulk mails to send per cycle. You can override this by defining a constant BULK_MAILS_PER_CYCLE in constants.php, but setting too high may cause timeouts.
* @brief Flag to denote just getting the count only.
*/
public $perCycle = 500;
/**
* Constructor - Create a blank object with default values
*
* @return void
*/
public function __construct()
{
/* Do we want to send a different number of bulk emails per cycle? */
if( defined( '\IPS\BULK_MAILS_PER_CYCLE' ) AND (int) \IPS\BULK_MAILS_PER_CYCLE > 0 )
{
$this->perCycle = (int) \IPS\BULK_MAILS_PER_CYCLE;
}
return parent::__construct();
}
/**
* Enable or disable the bulk mail task
*
* @param int $force 0 or 1 to specify the task state or NULL to determine automatically
* @return void
*/
public static function updateTask( $force=NULL )
{
/* Using Mandrill? */
if( ( \IPS\Settings::i()->mandrill_api_key && \IPS\Settings::i()->mandrill_use_for ) AND $force === NULL )
{
$force = 0;
}
/* Are we forcing the task enabled or disabled? */
if( $force !== NULL )
{
\IPS\DB::i()->update( 'core_tasks', array( 'enabled' => (int) $force ), "`key`='bulkmail'" );
return;
}
/* Figure out if we have any bulk mails to send and update task appropriately */
if( \IPS\DB::i()->select( 'count(*)', 'core_bulk_mail', 'mail_active=1' )->first() > 0 )
{
/* Enable the task */
\IPS\DB::i()->update( 'core_tasks', array( 'enabled' => 1 ), "`key`='bulkmail'" );
}
else
{
/* Disable the task */
\IPS\DB::i()->update( 'core_tasks', array( 'enabled' => 0 ), "`key`='bulkmail'" );
}
}
const GET_COUNT_ONLY = 1;
/**
* Get the mail options
*
@@ -120,227 +70,103 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
}
/**
* Send the bulk mail. Determines if bulk mail should be sent via Mandrill or internally and dispatches appropriately.
* Send the bulk mail. Determines if bulk mail should be sent via a task or immediately and dispatches appropriately.
*
* @return NULL|int
* @throws \Exception
*/
public function send()
{
/* Mandrill can handle 2000 users in one cycle */
if( \IPS\Settings::i()->mandrill_api_key AND \IPS\Settings::i()->mandrill_use_for )
{
$limit = array( $this->_data['sentto'], 2000 );
}
else
{
$limit = array( $this->_data['sentto'], $this->perCycle );
}
/* Work out how many to do */
$classToUse = \IPS\Email::classToUse( \IPS\Email::TYPE_BULK );
$limit = array( $this->offset, $classToUse::MAX_EMAILS_PER_GO );
/* Get recipients */
$results = $this->getQuery( $limit );
$results = $this->getQuery( $limit );
/* If there are no recipients we must be done */
if( !count( $results ) )
{
self::updateTask( 0 );
$this->active = 0;
$this->offset = 0;
$this->save();
return NULL;
}
/* Dispatch */
if( \IPS\Settings::i()->mandrill_api_key AND \IPS\Settings::i()->mandrill_use_for )
/* Convert $results into an array with replacement tags */
$recipients = array();
foreach ( $results as $memberData )
{
$sent = $this->_sendMandrill( $results );
$member = \IPS\Member::constructFromData( $memberData );
$vars = array();
foreach ( $this->returnTagValues( 2, $member ) as $k => $v )
{
$vars[ mb_substr( $k, 1, -1 ) ] = $v;
}
$recipients[ $member->language()->_id ][ $memberData['email'] ] = $vars;
}
else
/* Convert member-specific {{tag}} into *|tag|* and global {{tag}} into the value */
$content = $this->_data['content'];
foreach ( $this->returnTagValues( 1 ) as $k => $v )
{
$sent = $this->_sendInternal( $results );
$content = str_replace( $k, $v, $content );
}
foreach( array_keys( static::getTags() ) as $k )
{
if ( mb_strpos( $content, $k ) !== FALSE )
{
$content = str_replace( $k, '*|' . str_replace( array( '{', '}' ), '', $k ) . '|*', $content );
}
}
/* Send it */
$email = \IPS\Email::buildFromContent( $this->_data['subject'], $content, NULL, \IPS\Email::TYPE_BULK )
->setUnsubscribe( 'core', 'unsubscribeBulk' );
$sent = 0;
foreach ( $recipients as $languageId => $_recipients )
{
$sent += $email->mergeAndSend( $_recipients, NULL, NULL, array( 'List-Unsubscribe' => '<*|unsubscribe_url|*>' ), \IPS\Lang::load( $languageId ) );
}
/* Update bulk mail record */
if( $sent === 0 )
{
$this->active = 0;
$this->updated = time();
$this->offset = 0;
$this->save();
self::updateTask( 0 );
}
else
{
$this->updated = time();
$this->sentto = ( $this->_data['sentto'] + $sent );
$this->offset = ( $this->offset + $classToUse::MAX_EMAILS_PER_GO );
$this->sentto = ( $this->sentto + $sent );
$this->save();
}
/* Return the number of users the email was sent to */
return $sent;
}
/**
* Send the bulk mail using our own internal mailer
*
* @param \IPS\Db\Select $recipients Result set for recipients for the bulk mail
* @return int Number of users the bulk mail was sent to
*/
protected function _sendInternal( $recipients=array() )
{
$sent = 0;
/* Loop over recipients and send the email */
foreach( $recipients as $recipient )
{
$_recipient = \IPS\Member::constructFromData( $recipient );
$htmlBody = $this->formatBody( $_recipient );
$email = \IPS\Email::buildFromContent( $this->_data['subject'], $htmlBody );
$email->headers['Precedence'] = 'bulk';
$email->buildUnsubscribe( 'bulk', $_recipient );
$email->send( $_recipient );
$sent++;
}
return $sent;
}
/**
* Send the bulk mail using Mandrill
*
* @param \IPS\Db\Select $recipients Result set for recipients for the bulk mail
* @return int Number of users the bulk mail was sent to
* @throws \RuntimeException
*/
protected function _sendMandrill( $recipients=array() )
{
$sent = 0;
/* Get the email we want to send in the HTML template - we intentionally do not swap out tags as Mandrill does this for us. */
$email = \IPS\Email::buildFromContent( $this->_data['subject'], $this->_data['content'] );
$email->unsubscribe = array( 'link' => \IPS\Http\Url::internal( '' ), 'text' => '*|unsubscribe|*' );
$body = $email->body['html'];
/* Determine the variables to use */
$usedVars = array( 'unsubscribe' );
foreach( array_keys( self::getTags() ) as $k )
{
if ( mb_strpos( $body, $k ) !== FALSE )
{
$usedVars[] = $k;
/* While we're not actually parsing the tags, we do need to put into Mandrill format */
$body = str_replace( $k, '*|' . str_replace( array( '{', '}' ), '', $k ) . '|*', $body );
}
}
/* Loop over recipients and collect information */
$recipientsTo = array();
$recipientsMerge = array();
foreach( $recipients as $recipient )
{
$_recipient = \IPS\Member::constructFromData( $recipient );
/* While registration and other similar routines should validate the email, we need to filter out any bad ones that may have crept through else it causes Mandrill to ignore the batch */
if ( !$_recipient->email OR !$_recipient->name OR filter_var( $_recipient->email, FILTER_VALIDATE_EMAIL ) === FALSE )
{
continue;
}
/* Put our recipients into an array */
$recipientsTo[] = array( 'email' => $_recipient->email, 'name' => $_recipient->name );
/* Format variables */
$vars = array();
foreach( $this->returnTagValues( 2, $_recipient ) as $k => $v )
{
if ( in_array( $k, $usedVars ) )
{
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $v );
$vars[] = array( 'name' => str_replace( array( '{', '}' ), '', $k ), 'content' => $v );
}
}
$email->buildUnsubscribe( 'bulk', $_recipient );
$langKey = $email->unsubscribe['text'];
$vars[] = array( 'name' => 'unsubscribe', 'content' => "<a href='" . $email->unsubscribe['link'] . "'>" . $_recipient->language()->get( $langKey ) . "</a>" );
if ( !empty( $vars ) )
{
$recipientsMerge[] = array( 'rcpt' => $recipient['email'], 'vars' => $vars );
}
$sent++;
}
/* Sort out global vars */
$globalMergeVars = array();
foreach ( $this->returnTagValues( 1 ) as $k => $v )
{
if ( in_array( $k, $usedVars ) )
{
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $v );
$globalMergeVars[] = array( 'name' => str_replace( array( '{', '}' ), '', $k ), 'content' => $v );
}
}
/* Fix language strings */
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $body );
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $this->_data['subject'] );
/* Replace attachment and emoticon URLs */
\IPS\Output::i()->parseFileObjectUrls( $body );
$response = \IPS\Email::mandrill( 'messages_send', \IPS\Settings::i()->mandrill_api_key, array(
'message' => array(
'html' => $body,
'subject' => $this->_data['subject'],
'from_email' => \IPS\Settings::i()->email_out,
'from_name' => \IPS\Settings::i()->board_name,
'to' => $recipientsTo,
'auto_text' => TRUE,
'url_strip_qs' => FALSE,
'preserve_recipients' => FALSE,
'merge' => TRUE,
'global_merge_vars' => $globalMergeVars,
'merge_vars' => $recipientsMerge,
'tags' => array_merge( array( 'ips' ), array_filter( $this->_options['mandrill_tags'], function( $v ) { return (bool) $v; } ) )
),
'async' => TRUE
) );
if ( isset( $response->status ) and $response->status == 'error' )
{
throw new \UnexpectedValueException( \IPS\Member::loggedIn()->language()->addToStack('mandrill_error', FALSE, array( 'sprintf' => array( $response->message ) ) ) );
}
return $sent;
/* Return the number to increase the offset by (not necessarily the number of users the email was sent to, but still the amount we need to increase the offset by) */
return $classToUse::MAX_EMAILS_PER_GO;
}
/**
* Retrieve the query to fetch members based on our filters
*
* @param array $limit The limit to apply to the query
* @param array|int $option A constant to fetch counts only or an array with the limit to apply to the query
* @return \IPS\Db\Select
* @note Returns a default limit of $this->perCycle which should be overridden typically. The default limit is simply to ensure we don't accidentally run a query against the members table without any limit if someone forgets.
*/
public function getQuery( $limit=array() )
public function getQuery( $option=NULL )
{
/* Compile where */
$where = array();
$where[] = array( "core_members.allow_admin_mails=1" );
$where[] = array( "core_members.temp_ban=0" );
foreach ( \IPS\Application::allExtensions( 'core', 'BulkMail', FALSE, 'core' ) as $key => $extension )
$where[] = array( "core_members.email!=''" );
foreach ( \IPS\Application::allExtensions( 'core', 'MemberFilter', FALSE, 'core' ) as $key => $extension )
{
if( method_exists( $extension, 'getQueryWhereClause' ) )
{
@@ -361,10 +187,12 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
}
/* Compile query */
$query = \IPS\Db::i()->select( 'core_members.member_id AS my_member_id, core_members.*', 'core_members', $where, 'core_members.member_id', $limit, 'member_id', NULL, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS );
$select = ( $option !== static::GET_COUNT_ONLY ) ? 'core_members.member_id AS my_member_id, core_members.*' : 'COUNT(*)';
$limit = is_array( $option ) ? $option : NULL;
$query = \IPS\Db::i()->select( $select, 'core_members', $where, ( $option !== static::GET_COUNT_ONLY ) ? 'core_members.member_id' : NULL, $limit );
/* Run callbacks */
foreach ( \IPS\Application::allExtensions( 'core', 'BulkMail', TRUE, 'core' ) as $key => $extension )
foreach ( \IPS\Application::allExtensions( 'core', 'MemberFilter', FALSE, 'core' ) as $key => $extension )
{
if( method_exists( $extension, 'queryCallback' ) )
{
@@ -376,38 +204,10 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
}
}
}
return $query;
}
/**
* Format a bulk mail by replacing out the tags with the proper values
*
* @param \IPS\Member $member Member data
* @return string
*/
public function formatBody( $member )
{
if( empty($this->_data['content']) )
{
return '';
}
/* Default tags */
$tags = $this->returnTagValues( 0, $member );
/* Work on a copy rather than the original template */
$body = $this->_data['content'];
/* Loop over the tags and swap out as appropriate */
foreach( $tags as $key => $value )
{
$body = str_replace( $key, $value, $body );
}
return $body;
}
/**
* Return tag values
*
@@ -415,7 +215,7 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
* @param NULL|\IPS\Member $member Member object if $type is 0 or 2
* @return array
*/
protected function returnTagValues( $type, $member=NULL )
public function returnTagValues( $type, $member=NULL )
{
$tags = array();
@@ -432,7 +232,7 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
/* Only bother querying if we need the value */
if( mb_strpos( $this->_data['content'], '{reg_total}' ) !== FALSE )
{
$tags['{reg_total}'] = \IPS\DB::i()->select( 'count(*)', 'core_members', 'member_id > 0' )->first();
$tags['{reg_total}'] = \IPS\Db::i()->select( 'count(*)', 'core_members', 'member_id > 0' )->first();
}
}
@@ -440,10 +240,16 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
if( $type === 0 OR $type === 2 )
{
$tags['{member_id}'] = $member->member_id;
$tags['{member_email}'] = $member->email;
$tags['{member_name}'] = $member->name;
$tags['{member_joined}'] = $member->joined->localeDate();
$tags['{member_last_visit}'] = \IPS\DateTime::ts( $member->last_visit )->localeDate();
$tags['{member_posts}'] = $member->member_posts;
$tags['{unsubscribe_url}'] = (string) \IPS\Http\Url::internal( 'app=core&module=system&controller=unsubscribe', 'front', 'unsubscribe' )->setQueryString( array(
'email' => $member->email,
'key' => md5( $member->email . ':' . $member->members_pass_hash )
) );
$tags['{unsubscribe_key}'] = md5( $member->email . ':' . $member->members_pass_hash );
}
/* Now retrieve tags via any bulk mail extensions. We only want them to return an array of tags to perform formatting, but
@@ -500,7 +306,20 @@ class _Bulkmailer extends \IPS\Patterns\ActiveRecord
*/
public function delete()
{
/* Unclaim Attachments */
\IPS\File::unclaimAttachments( 'core_Admin', $this->id, NULL, 'bulkmail' );
/* Make sure any queue rows are removed */
foreach( \IPS\Db::i()->select( '*', 'core_queue', array( "`key`=?", 'Bulkmail' ) ) AS $task )
{
$data = json_decode( $task['data'], TRUE );
if ( isset( $data['mail_id'] ) AND $data['mail_id'] == $this->id )
{
\IPS\Db::i()->delete( 'core_queue', array( "id=?", $task['id'] ) );
}
}
parent::delete();
}
}