Seditio Source
Root |
./othercms/ips_4.3.4/system/Email/Outgoing/SendGrid.php
<?php
/**
 * @brief        SendGrid Email Class
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @since        17 Oct 2013
 */

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( array( 'message' => $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( array( 'message' => $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' );
    }
   
}