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

+39 -34
View File
@@ -2,9 +2,9 @@
/**
* @brief Debug Email Class
* @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 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
@@ -26,45 +26,50 @@ class _Debug extends \IPS\Email
/**
* @brief Debug path to write the email files
*/
public $debugPath = '';
protected $debugPath = '';
/**
* Send
*
* @return bool
* @throws \IPS\Email\Exception
* @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
*/
protected function _send()
const REQUIRES_TIME_BREAK = TRUE;
/**
* Constructor
*
* @param string $debugPath Debug path
* @return void
*/
public function __construct( $debugPath )
{
$this->debugPath = $debugPath;
}
/**
* Send the email
*
* @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)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
if( !is_dir( $this->debugPath ) )
{
throw new \IPS\Email\Outgoing\Exception( sprintf( \IPS\Member::loggedIn()->language()->get('no_path_email_debug'), $this->debugPath ) );
}
$debugString = $this->subject . "\n";
$debugString .= "To: " . implode( ', ', $this->to ) . "\n";
$debugString .= str_repeat( '-', 40 ) . "\n";
$debugString .= $this->_compileHeaders() . "\n";
$debugString .= str_repeat( '-', 40 ) . "\n";
$debugString .= $this->message;
$fileName = date("M-j-Y") . '-' . microtime() . '-' . urlencode( mb_substr( implode( ',', $this->to ), 0, 200 ) ) . ".eml";
$this->writeLog( $fileName, $debugString );
return TRUE;
}
/**
* Write the debug file
*
* @param string $filename File name
* @param string $data Data to write to the file
* @return void
*/
protected function writeLog( $filename, $data )
{
if( !@\file_put_contents( rtrim( $this->debugPath, '/' ) . '/' . $filename, $data ) )
$fullEmailContents = $this->compileFullEmail( $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders );
$filename = date("M-j-Y") . '-' . microtime() . '-' . urlencode( mb_substr( $this->_parseRecipients( $to, TRUE ), 0, 200 ) ) . ".eml";
if( !@\file_put_contents( rtrim( $this->debugPath, '/' ) . '/' . $filename, $fullEmailContents ) )
{
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'no_write_email_debug', FALSE, array( 'sprintf' => array( rtrim( $this->debugPath, '/' ) . '/' . $filename ) ) ) );
}
+2 -2
View File
@@ -2,9 +2,9 @@
/**
* @brief Exception class for email errors
* @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 26 Aug 2013
* @version SVN_VERSION_NUMBER
*/
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* @brief IPS CiC Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 13 Dec 2016
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Outgoing;
/* 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;
}
/**
* IPS CiC Email Class
*/
class _IpsCic extends \IPS\Email
{
/* !Configuration */
/**
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = 200;
/**
* @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 = FALSE;
/**
* @brief IPS Cloud Email Endpoint
*/
const ENDPOINT = 'http://ips-cic-email.invisioncic.com/sendEmail.php';
/**
* @brief Community
*/
protected $community;
/**
* Constructor
*
* @param string $apiKey API Key
* @return void
*/
public function __construct()
{
if ( \IPS\CIC and preg_match( '/^\/var\/www\/html\/(.+?)(?:\/|$)/i', \IPS\ROOT_PATH, $matches ) )
{
$this->community = $matches[1];
}
else
{
throw new \RuntimeException('Site not compatible with IPS Cloud Email');
}
}
/**
* Send the email
*
* @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)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
\IPS\Http\Url::external( static::ENDPOINT )->request()->post( array(
'siteId' => $this->community,
'to' => explode( ',', static::_parseRecipients( $to, TRUE ) ),
'cc' => explode( ',', static::_parseRecipients( $cc, TRUE ) ),
'bcc' => explode( ',', static::_parseRecipients( $bcc, TRUE ) ),
'fromEmail' => $fromEmail ?: \IPS\Settings::i()->email_out,
'fromName' => $fromName ?: \IPS\Settings::i()->board_name,
'additionalHeaders' => $additionalHeaders,
'subject' => $this->compileSubject( static::_getMemberFromRecipients( $to ) ),
'html' => $this->compileContent( 'html', static::_getMemberFromRecipients( $to ) ),
'plaintext' => $this->compileContent( 'plaintext', static::_getMemberFromRecipients( $to ) ),
'precedence' => ( $this->type === static::TYPE_LIST ) ? 'list' : ( $this->type === static::TYPE_BULK ? 'bulk' : '' )
) );
}
}
+27 -10
View File
@@ -2,9 +2,9 @@
/**
* @brief PHP Email Class
* @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 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
@@ -24,15 +24,34 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
class _PHP extends \IPS\Email
{
/**
* Send
*
* Send the email
*
* @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)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _send()
{
try
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
$boundary = "--==_mimepart_" . md5( uniqid() );
$subject = $this->compileSubject( static::_getMemberFromRecipients( $to ) );
$headers = array();
foreach( $this->_compileHeaders( $subject, $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders, $boundary ) as $k => $v )
{
if ( !mail( $this->headers['To'], mb_encode_mimeheader( $this->subject ), $this->message, $this->_compileHeaders( TRUE ), \IPS\Settings::i()->php_mail_extra ) )
if ( !in_array( $k, array( 'To', 'Subject' ) ) )
{
$headers[] = "{$k}: {$v}";
}
}
try
{
if ( !mail( $this->_parseRecipients( $to, TRUE ), static::encodeHeader( $subject ), $this->_compileMessage( static::_getMemberFromRecipients( $to ), $boundary, PHP_EOL ), implode( PHP_EOL, $headers ), \IPS\Settings::i()->php_mail_extra ) )
{
$error = error_get_last();
preg_match( "/^(\d+)/", $error['message'], $matches );
@@ -43,7 +62,5 @@ class _PHP extends \IPS\Email
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage() );
}
return TRUE;
}
}
+369
View File
@@ -0,0 +1,369 @@
<?php
/**
* @brief SendGrid Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 17 Oct 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Outgoing;
/* 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;
}
/**
* SendGrid Email Class
*/
class _SendGrid extends \IPS\Email
{
/* !Configuration */
/**
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = 1000; // SendGrid has a hard 1000 recipients per request limit
/**
* @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 = FALSE;
/**
* @brief API Key
*/
protected $apiKey;
/**
* Constructor
*
* @param string $apiKey API Key
* @return void
*/
public function __construct( $apiKey )
{
$this->apiKey = $apiKey;
}
/**
* Send the email
*
* @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)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
/* Initiate the request */
$request = $this->_initRequest( $fromEmail, $fromName );
/* Add the subject and content */
$request['subject'] = $this->compileSubject( static::_getMemberFromRecipients( $to ) );
$request['content'] = array(
array(
'type' => 'text/plain',
'value' => $this->compileContent( 'plaintext', static::_getMemberFromRecipients( $to ) )
),
array(
'type' => 'text/html',
'value' => $this->compileContent( 'html', static::_getMemberFromRecipients( $to ) )
),
);
/* Add the recipients */
foreach ( array( 'to', 'cc', 'bcc' ) as $type )
{
if ( is_array( $$type ) )
{
foreach ( $$type as $recipient )
{
if ( $recipient instanceof \IPS\Member )
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient->email, 'name' => $recipient->name );
}
else
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient );
}
}
}
elseif ( $$type )
{
$recipient = $$type;
if ( $recipient instanceof \IPS\Member )
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient->email, 'name' => $recipient->name );
}
else
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient );
}
}
}
/* Add additional headers */
$request = $this->_modifyRequestDataWithHeaders( $request, $additionalHeaders );
/* Send */
$response = $this->_api( 'mail/send', $request );
if ( isset( $response['errors'] ) )
{
throw new \IPS\Email\Outgoing\Exception( $response['errors'][0]['message'] );
}
}
/**
* 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
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS|Lang $language The language the email content should be in
* @return int Number of successful sends
*/
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), \IPS\Lang $language )
{
/* Initiate the request */
$request = $this->_initRequest( $fromEmail, $fromName );
/* Add the subject and content */
$subject = $this->compileSubject( NULL, $language );
$htmlContent = $this->compileContent( 'html', FALSE, $language );
$plaintextContent = $this->compileContent( 'plaintext', FALSE, $language );
$request['subject'] = $subject;
$request['content'] = array(
array(
'type' => 'text/plain',
'value' => $plaintextContent
),
array(
'type' => 'text/html',
'value' => $htmlContent
),
);
/* Add the recipients */
$addresses = array();
foreach ( $recipients as $email => $substitutions )
{
$addresses[] = $email;
$finalSubstitutions = array();
foreach ( $substitutions as $k => $v )
{
$language->parseEmail( $v );
$finalSubstitutions["*|{$k}|*"] = $v;
}
$request['personalizations'][] = array(
'to' => array( array( 'email' => $email ) ),
'substitutions' => $finalSubstitutions
);
}
/* Add additional headers */
$request = $this->_modifyRequestDataWithHeaders( $request, $additionalHeaders );
/* Send */
try
{
$response = $this->_api( 'mail/send', $request );
}
catch( \IPS\Email\Outgoing\Exception $e )
{
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $e->getMessage() ),
'mlog_smtp_log' => NULL
) );
return 0;
}
$errorcount = 0;
if ( isset( $response['errors'] ) )
{
$errorcount = count( $response['errors'] );
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $response['errors'] ),
'mlog_smtp_log' => NULL
) );
}
return ( count( $recipients ) - $errorcount );
}
/**
* Create a request
*
* @param array $recipients Array where the keys are the email addresses to send to and the values are an array of variables to replace
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS|Lang $language The language the email content should be in
* @return array
*/
protected function _initRequest( $fromEmail = NULL, $fromName = NULL )
{
$request = array(
'personalizations' => array(),
'from' => array(
'email' => $fromEmail ?: \IPS\Settings::i()->email_out,
'name' => $fromName ?: \IPS\Settings::i()->board_name
),
'tracking_settings' => array(
'click_tracking' => array(
'enable' => (bool) \IPS\Settings::i()->sendgrid_click_tracking,
'enable_text' => (bool) \IPS\Settings::i()->sendgrid_click_tracking,
),
'open_tracking' => array(
'enable' => (bool) \IPS\Settings::i()->sendgrid_click_tracking,
)
)
);
if ( \IPS\Settings::i()->sendgrid_ip_pool )
{
$request['ip_pool_name'] = (bool) \IPS\Settings::i()->sendgrid_ip_pool;
}
return $request;
}
/**
* Modify the request data that will be sent to the SparkPost API with header data
*
* @param array $request SparkPost API request data
* @param array $additionalHeaders Additional headers to send
* @param array $allowedTags The tags that we want to parse
* @return array
*/
protected function _modifyRequestDataWithHeaders( $request, $additionalHeaders = array(), $allowedTags = array() )
{
/* Do we have a Reply-To? */
if ( isset( $additionalHeaders['Reply-To'] ) )
{
if ( preg_match( '/(.*)\s?<(.*)>$/', $additionalHeaders['Reply-To'], $matches ) )
{
$email = $matches[2];
$request['reply_to'] = array( 'email' => $matches[2] );
if ( $matches[1] )
{
if ( preg_match( '/^\=\?UTF\-8\?B\?(.+?)\?\=$/i', trim( $matches[1] ), $_matches ) )
{
$request['reply_to']['name'] = base64_decode( $_matches[1] );
}
}
}
unset( $additionalHeaders['Reply-To'] );
}
/* Any other headers? */
unset( $additionalHeaders['x-sg-id'] );
unset( $additionalHeaders['x-sg-eid'] );
unset( $additionalHeaders['received'] );
unset( $additionalHeaders['dkim-signature'] );
unset( $additionalHeaders['Content-Type'] );
unset( $additionalHeaders['Content-Transfer-Encoding'] );
unset( $additionalHeaders['Subject'] );
unset( $additionalHeaders['From'] );
unset( $additionalHeaders['To'] );
unset( $additionalHeaders['CC'] );
unset( $additionalHeaders['BCC'] );
if ( count( $additionalHeaders ) )
{
$request['headers'] = $additionalHeaders;
}
/* Return */
return $request;
}
/**
* Make API call
*
* @param string $method Method
* @param string $apiKey API Key
* @param array $args Arguments
* @throws \IPS\Email\Outgoing\Exception Indicates an invalid JSON response or HTTP error
* @return array|null
*/
protected function _api( $method, $args=NULL )
{
$request = \IPS\Http\Url::external( 'https://api.sendgrid.com/v3/' . $method )
->request( \IPS\LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json', 'Authorization' => "Bearer {$this->apiKey}" ) );
try
{
if ( $args )
{
$response = $request->post( json_encode( $args ) );
}
else
{
$response = $request->get();
}
if ( $response->content )
{
$response = $response->decodeJson();
}
else
{
$response = null;
}
return $response;
}
catch ( \IPS\Http\Request\Exception $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
/* Capture json decoding errors */
catch ( \RuntimeException $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
}
/**
* Get API key scopes
*
* @return array
*/
public function scopes()
{
return $this->_api( 'scopes' );
}
}
+130 -60
View File
@@ -2,9 +2,9 @@
/**
* @brief SMTP Email Class
* @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 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
@@ -24,41 +24,60 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
class _SMTP extends \IPS\Email
{
/**
* @brief SMTP Conversation Log
* @brief SMTP Protocol ("tls", "ssl" or "plain")
*/
public $log;
protected $smtpProtocol;
/**
* @brief SMTP Host
*/
protected $smtpHost;
/**
* @brief SMTP Port
*/
protected $smtpPort;
/**
* @brief SMTP Username
*/
protected $smtpUser;
/**
* @brief SMTP Password
*/
protected $smtpPass;
/**
* @brief SMTP Connections
*/
protected static $smtp = array();
/**
* @brief Connection Key
*/
protected $connectionKey;
/**
* @brief Log
*/
protected $log = '';
/**
* Constructor
*
* @return void
*/
public function __construct()
public function __construct( $smtpProtocol, $smtpHost, $smtpPort, $smtpUser, $smtpPass )
{
$this->log = '';
$this->smtpProtocol = $smtpProtocol;
$this->smtpHost = $smtpHost;
$this->smtpPort = $smtpPort;
$this->smtpUser = $smtpUser;
$this->smtpPass = $smtpPass;
$this->connectionKey = md5( $smtpProtocol . $smtpHost . $smtpPort . $smtpUser . $smtpPass );
}
/**
* Destructor
*
* @return void
*/
public function __destruct()
{
if( static::$smtp !== NULL )
{
$this->_sendCommand( 'quit' );
@fclose( static::$smtp );
static::$smtp = NULL;
}
}
/**
* @brief SMTP Connection
*/
protected static $smtp = NULL;
/**
* Connect to server
*
@@ -67,25 +86,27 @@ class _SMTP extends \IPS\Email
public function connect()
{
/* Do we already have a connection? */
if( static::$smtp !== NULL )
if( array_key_exists( $this->connectionKey, static::$smtp ) )
{
return;
}
/* Connect */
static::$smtp = @fsockopen( ( ( \IPS\Settings::i()->smtp_protocol == 'ssl' ) ? 'ssl://' : '' ) . \IPS\Settings::i()->smtp_host, \IPS\Settings::i()->smtp_port, $errno, $errstr );
if ( !static::$smtp )
$connection = @fsockopen( ( ( $this->smtpProtocol == 'ssl' ) ? 'ssl://' : '' ) . $this->smtpHost, $this->smtpPort, $errno, $errstr );
if ( !$connection )
{
static::$smtp = NULL;
throw new \IPS\Email\Outgoing\Exception( $errstr, $errno );
}
static::$smtp[ $this->connectionKey ] = $connection;
register_shutdown_function(function( $object ){
$object->_sendCommand( 'quit' );
@fclose( static::$smtp[ $object->connectionKey ] );
unset( static::$smtp[ $object->connectionKey ] );
}, $this );
/* Check the initial response is okay */
$announce = $this->_getResponse();
$this->log .= $announce;
$responseCode = mb_substr( $announce, 0, 3 );
if ( $responseCode != 220 )
{
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack('smtpmail_fsock_error_initial', FALSE, array( 'sprintf' => array( $responseCode ) ) ) );
@@ -95,79 +116,124 @@ class _SMTP extends \IPS\Email
try
{
$helo = 'EHLO';
$responseCode = $this->_sendCommand( 'EHLO ' . \IPS\Settings::i()->smtp_host, 250 );
$responseCode = $this->_sendCommand( 'EHLO ' . $this->smtpHost, 250 );
}
catch ( \IPS\Email\Outgoing\Exception $e )
{
$helo = 'HELO';
$responseCode = $this->_sendCommand( 'HELO ' . \IPS\Settings::i()->smtp_host, 250 );
$responseCode = $this->_sendCommand( 'HELO ' . $this->smtpHost, 250 );
}
/* Is TLS being used? */
if( \IPS\Settings::i()->smtp_protocol == 'tls' )
if( $this->smtpProtocol == 'tls' )
{
$this->_sendCommand( 'STARTTLS', 220 );
if ( !@\stream_socket_enable_crypto( static::$smtp, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT ) )
if ( !@\stream_socket_enable_crypto( static::$smtp[ $this->connectionKey ], TRUE, version_compare( PHP_VERSION, '5.6.7' ) >= 0 ? STREAM_CRYPTO_METHOD_SSLv23_CLIENT : STREAM_CRYPTO_METHOD_TLS_CLIENT ) )
{
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack('smtpmail_tls_failed') );
}
/* Exchange server (at least) wants EHLO resending for STARTTLS */
$this->_sendCommand( $helo . ' ' . \IPS\Settings::i()->smtp_host, 250 );
$this->_sendCommand( $helo . ' ' . $this->smtpHost, 250 );
}
/* Authenticate */
if ( \IPS\Settings::i()->smtp_user )
if ( $this->smtpUser )
{
$responseCode = $this->_sendCommand( 'AUTH LOGIN', 334 );
$responseCode = $this->_sendCommand( base64_encode( \IPS\Settings::i()->smtp_user ), 334 );
$responseCode = $this->_sendCommand( base64_encode( \IPS\Settings::i()->smtp_pass ), 235 );
$responseCode = $this->_sendCommand( base64_encode( $this->smtpUser ), 334 );
$responseCode = $this->_sendCommand( base64_encode( $this->smtpPass ), 235 );
}
}
/**
* Send
*
* @throws \Exception
* @note We use $this->from here instead of the 'From' header, because the header may have a display name embedded
* Send the email
*
* @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)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _send()
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
/* Get the from email */
$fromEmail = $fromEmail ?: \IPS\Settings::i()->email_out;
/* SMTP requires you to do CC/BCC by sending a RCPT TO command for each recipient. We'll hide BCC by not actually setting that header */
$recipieintsForSmtp = explode( ',', static::_parseRecipients( $to, TRUE ) );
if ( $cc )
{
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $cc, TRUE ) ) );
}
if ( $bcc )
{
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $bcc, TRUE ) ) );
}
$recipieintsForSmtp = array_unique( array_map( 'trim', $recipieintsForSmtp ) );
/* Send */
$this->_sendCompiled( $fromEmail, $recipieintsForSmtp, $this->compileFullEmail( $to, $cc, array(), $fromEmail, $fromName, $additionalHeaders ) );
}
/**
* Send an email
*
* @param string $fromEmail The email address to send from
* @param array $toEmails Array of email addresses to send to
* @param string $email The full email (with headers, etc.) except the Bcc header
* @param array $additionalHeaders Additional headers to send
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _sendCompiled( $fromEmail, $toEmails, $email )
{
$this->connect();
$this->_sendCommand( "MAIL FROM:<{$this->from}>", 250 );
foreach ( $this->to as $to )
$this->_sendCommand( "MAIL FROM:<{$fromEmail}>", 250 );
foreach ( $toEmails as $toEmail )
{
$this->_sendCommand( "RCPT TO:<{$to}>", 250 );
$this->_sendCommand( "RCPT TO:<{$toEmail}>", 250 );
}
$this->_sendCommand( 'DATA', 354 );
$this->_sendCommand( "{$this->_compileHeaders()}\r\n\r\n{$this->message}\r\n.", 250 );
$this->_sendCommand( $email . "\r\n.", 250 );
}
/**
* Send SMTP Command
*
* @param string $command The command
* @param int $expectedResponse The expected response code. Will throw an exception if different.
* @param string $command The command
* @param int|NULL $expectedResponse The expected response code. Will throw an exception if different.
* @param bool $resetOnFailure If the command fails, issue a RSET (reset) command
* @return string Response
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _sendCommand( $command, $expectedResponse=NULL )
protected function _sendCommand( $command, $expectedResponse=NULL, $resetOnFailure=FALSE )
{
/* Send */
/* Log */
$this->log .= "> {$command}\r\n";
fputs( static::$smtp, $command . "\r\n" );
/* Send */
fputs( static::$smtp[ $this->connectionKey ], $command . "\r\n" );
/* Read */
$response = $this->_getResponse();
$this->log .= $response;
/* Get response code */
$code = intval( mb_substr( $response, 0, 3 ) );
if ( $expectedResponse !== NULL and $code !== $expectedResponse )
{
if( $resetOnFailure === TRUE )
{
$this->_sendCommand( 'RSET' );
}
throw new \IPS\Email\Outgoing\Exception( $response, $code );
}
@@ -184,7 +250,7 @@ class _SMTP extends \IPS\Email
{
/* Read */
$response = '';
while ( $line = @fgets( static::$smtp, 515 ) )
while ( $line = @fgets( static::$smtp[ $this->connectionKey ], 515 ) )
{
$response .= $line;
if ( mb_substr($line, 3, 1) == " " )
@@ -192,10 +258,14 @@ class _SMTP extends \IPS\Email
break;
}
}
/* Log */
$this->log .= $response;
/* Return */
return $response;
}
/**
* Return the SMTP log
*
+358
View File
@@ -0,0 +1,358 @@
<?php
/**
* @brief SparkPost Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Outgoing;
/* 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;
}
/**
* SparkPost Email Class
*/
class _SparkPost extends \IPS\Email
{
/* !Configuration */
/**
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = 2000; // SparkPost "recommends" 2000 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 = FALSE;
/**
* @brief API Key
*/
protected $apiKey;
/**
* Constructor
*
* @param string $apiKey API Key
* @return void
*/
public function __construct( $apiKey )
{
$this->apiKey = $apiKey;
}
/**
* Send the email
*
* @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)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
/* Compile the recipients */
$recipients = array();
$mainToAddress = NULL;
foreach ( array_map( 'trim', explode( ',', static::_parseRecipients( $to, TRUE ) ) ) as $address )
{
if ( $mainToAddress === NULL )
{
$mainToAddress = $address;
}
$recipients[] = array( 'address' => array( 'email' => $address ) );
}
if ( $cc )
{
foreach ( array_map( 'trim', explode( ',', static::_parseRecipients( $cc, TRUE ) ) ) as $address )
{
$recipients[] = array( 'address' => array( 'email' => $address, 'header_to' => $mainToAddress ) );
}
$additionalHeaders['Cc'] = static::_parseRecipients( $cc );
}
if ( $bcc )
{
foreach ( array_map( 'trim', explode( ',', static::_parseRecipients( $bcc, TRUE ) ) ) as $address )
{
$recipients[] = array( 'address' => array( 'email' => $address, 'header_to' => $mainToAddress ) );
}
}
/* Compile the API call request data */
$request = array(
'recipients' => $recipients,
'content' => array(
'html' => static::_escapeTemplateTags( $this->compileContent( 'html', static::_getMemberFromRecipients( $to ) ) ),
'text' => static::_escapeTemplateTags( $this->compileContent( 'plaintext', static::_getMemberFromRecipients( $to ) ) ),
'subject' => static::_escapeTemplateTags( $this->compileSubject( static::_getMemberFromRecipients( $to ) ) ),
'from' => array(
'email' => $fromEmail ?: \IPS\Settings::i()->email_out,
'name' => $fromName ?: \IPS\Settings::i()->board_name
)
),
'options' => array(
'transactional' => $this->type === static::TYPE_TRANSACTIONAL,
'open_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking,
'click_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking
)
);
if ( \IPS\Settings::i()->sparkpost_ip_pool )
{
$request['options']['ip_pool'] = \IPS\Settings::i()->sparkpost_ip_pool;
}
$request = $this->_modifyRequestDataWithHeaders( $request, $additionalHeaders );
/* Make API call */
$response = $this->_api( 'transmissions', $request );
if ( isset( $response['errors'] ) )
{
throw new \IPS\Email\Outgoing\Exception( $response['errors'][0]['message'], ( isset( $response['errors'][0]['code'] ) ) ? $response['errors'][0]['code'] : NULL );
}
}
/**
* Modify the request data that will be sent to the SparkPost API with header data
*
* @param array $request SparkPost API request data
* @param array $additionalHeaders Additional headers to send
* @param array $allowedTags The tags that we want to parse
* @return array
*/
protected function _modifyRequestDataWithHeaders( $request, $additionalHeaders = array(), $allowedTags = array() )
{
/* Do we have a Reply-To? */
if ( isset( $additionalHeaders['Reply-To'] ) )
{
$request['content']['reply_to'] = static::_escapeTemplateTags( $additionalHeaders['Reply-To'], $allowedTags );
unset( $additionalHeaders['Reply-To'] );
}
/* Any other headers? */
unset( $additionalHeaders['Subject'] );
unset( $additionalHeaders['From'] );
unset( $additionalHeaders['To'] );
if ( count( $additionalHeaders ) )
{
$request['content']['headers'] = array_map( function( $v ) use ( $allowedTags ) {
return static::_escapeTemplateTags( $v, $allowedTags );
}, $additionalHeaders );
}
/* Return */
return $request;
}
/**
* 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
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS|Lang $language The language the email content should be in
* @return int Number of successful sends
*/
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), \IPS\Lang $language )
{
/* Work out recipients */
$varNames = array();
$recipientsForSparkpost = array();
$addresses = array();
foreach ( $recipients as $address => $_vars )
{
$addresses[] = $address;
$vars = array();
foreach ( $_vars as $k => $v )
{
$language->parseEmail( $v );
$vars[ $k ] = $v;
if ( !in_array( $k, $varNames ) )
{
$varNames[] = $k;
}
}
$recipientsForSparkpost[] = array( 'address' => array( 'email' => $address ), 'substitution_data' => $vars );
}
/* Put tags into SparkPost format */
$htmlContent = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $this->compileContent( 'html', FALSE, $language ) );
$plaintextContent = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $this->compileContent( 'plaintext', FALSE, $language ) );
$subject = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $this->compileSubject( NULL, $language ) );
$_additionalHeaders = array();
foreach ( $additionalHeaders as $k => $v )
{
$_additionalHeaders[ $k ] = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $v );
}
/* Compile the API call request data */
$request = array(
'recipients' => $recipientsForSparkpost,
'content' => array(
'html' => static::_escapeTemplateTags( $htmlContent, $varNames ),
'text' => static::_escapeTemplateTags( $plaintextContent, $varNames ),
'subject' => static::_escapeTemplateTags( $subject, $varNames ),
'from' => array(
'email' => $fromEmail ?: \IPS\Settings::i()->email_out,
'name' => $fromName ?: \IPS\Settings::i()->board_name
)
),
'options' => array(
'transactional' => $this->type === static::TYPE_TRANSACTIONAL,
'open_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking,
'click_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking
)
);
if ( \IPS\Settings::i()->sparkpost_ip_pool )
{
$request['options']['ip_pool'] = \IPS\Settings::i()->sparkpost_ip_pool;
}
$request = $this->_modifyRequestDataWithHeaders( $request, $_additionalHeaders, $varNames );
/* Make API call */
try
{
$response = $this->_api( 'transmissions', $request );
}
catch( \IPS\Email\Outgoing\Exception $e )
{
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $e->getMessage() ),
'mlog_smtp_log' => NULL
) );
return 0;
}
if ( isset( $response['errors'] ) )
{
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $response['errors'] ),
'mlog_smtp_log' => NULL
) );
}
return ( isset( $response['results'] ) and isset( $response['results']['total_accepted_recipients'] ) ) ? $response['results']['total_accepted_recipients'] : 0;
}
/**
* Make API call
*
* @param string $method Method
* @param string $apiKey API Key
* @param array $args Arguments
* @throws \IPS\Email\Outgoing\Exception Indicates an invalid JSON response or HTTP error
* @return array
*/
protected function _api( $method, $args=NULL )
{
$request = \IPS\Http\Url::external( 'https://api.sparkpost.com/api/v1/' . $method )
->request( \IPS\LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json', 'Authorization' => $this->apiKey ) );
try
{
if ( $args )
{
$response = $request->post( json_encode( $args ) );
}
else
{
$response = $request->get();
}
return $response->decodeJson();
}
catch ( \IPS\Http\Request\Exception $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
/* Capture json decoding errors */
catch ( \RuntimeException $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
}
/**
* Escape template tags
*
* @param string $content The content
* @param array $allowedTags The tags that we want to parse
* @return array
* @see <a href="https://developers.sparkpost.com/api/#/introduction/substitutions-reference/escaping-start-and-end-tags">Escaping Start and End Tags</a>
*/
protected static function _escapeTemplateTags( $content , $allowedTags = array() )
{
/* Store the tags we're allowed */
$replacedTagsI = 0;
$replacedTagsStore = array();
$content = preg_replace_callback( '/\{\{\{(' . implode( '|', array_map( function( $val ) { return preg_quote( $val, '/' ); }, $allowedTags ) ) . ')\}\}\}/', function( $matches ) use ( &$replacedTagsI, &$replacedTagsStore )
{
$replacedTagsStore[ ++$replacedTagsI ] = $matches[0];
return '--rt-' . $replacedTagsI . '--';
}, $content );
/* Escape */
$content = str_replace( '{{{', '--ot--x--', $content );
$content = str_replace( '}}}', '--ct--x--', $content );
$content = str_replace( '{{', '--od--x--', $content );
$content = str_replace( '}}', '--cd--x--', $content );
$content = str_replace( '--ot--x--', '{{opening_triple_curly()}}', $content );
$content = str_replace( '--ct--x--', '{{closing_triple_curly()}}', $content );
$content = str_replace( '--od--x--', '{{opening_double_curly()}}', $content );
$content = str_replace( '--cd--x--', '{{closing_double_curly()}}', $content );
/* Put our {{{tags}}} back */
$content = preg_replace_callback( '/--rt-(.+?)--/', function( $matches ) use ( $replacedTagsStore )
{
return isset( $replacedTagsStore[ $matches[1] ] ) ? $replacedTagsStore[ $matches[1] ] : '';
}, $content );
/* Return */
return $content;
}
/**
* Get sending domains
*
* @return array
*/
public function sendingDomains()
{
return $this->_api( 'sending-domains' );
}
}