Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
+418
-412
File diff suppressed because it is too large.
Load diff
@@ -1,881 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Incoming email parsing and routing
|
||||
* @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 12 June 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Email\Incoming;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP Email Class
|
||||
* @see <a href="https://tools.ietf.org/html/rfc5322">RFC 5322</a>
|
||||
*/
|
||||
class _Email
|
||||
{
|
||||
/**
|
||||
* @brief "To" email addresses
|
||||
*/
|
||||
public $to = array();
|
||||
|
||||
/**
|
||||
* @brief "CC" email addresses
|
||||
*/
|
||||
public $cc = array();
|
||||
|
||||
/**
|
||||
* @brief "From" email address
|
||||
*/
|
||||
public $from = '';
|
||||
|
||||
/**
|
||||
* @brief Subject
|
||||
*/
|
||||
public $subject = '';
|
||||
|
||||
/**
|
||||
* @brief Message (sanatised HTML)
|
||||
*/
|
||||
public $message = '';
|
||||
|
||||
/**
|
||||
* @brief Quoted part of the message (sanatised HTML)
|
||||
*/
|
||||
public $quote = '';
|
||||
|
||||
/**
|
||||
* @brief Attachments
|
||||
*/
|
||||
public $attachments = array();
|
||||
|
||||
/**
|
||||
* @brief Existing attachments to store a map for
|
||||
*/
|
||||
public $mappedAttachments = array();
|
||||
|
||||
/**
|
||||
* @brief Raw email headers
|
||||
*/
|
||||
public $headers = array();
|
||||
|
||||
/**
|
||||
* @brief Raw email contents
|
||||
*/
|
||||
public $raw = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $contents Contents
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $contents )
|
||||
{
|
||||
/* Extract the raw data */
|
||||
$this->raw = $contents;
|
||||
$data = $this->_decodePart( $contents );
|
||||
|
||||
/* Parse headers into something we can use */
|
||||
$this->headers = $data['headers'];
|
||||
$this->_parseBasicHeaders( $data['headers'] );
|
||||
|
||||
/* Get the message contents */
|
||||
$this->_parseMessagePart( $data );
|
||||
}
|
||||
|
||||
/* !Raw Email Parsing */
|
||||
|
||||
/**
|
||||
* Decode message part
|
||||
*
|
||||
* @param string $contents Contents
|
||||
* @return array headers, parts, body
|
||||
*/
|
||||
protected function _decodePart( $contents )
|
||||
{
|
||||
/* Separate header and body */
|
||||
preg_match( "/^(.*?)\r?\n\r?\n(.*)/s", $contents, $matches );
|
||||
$header = $matches[1];
|
||||
$body = $matches[2];
|
||||
|
||||
/* Unfold headers (see section 2.2.3 of the RFC) */
|
||||
$header = preg_replace( "/\r?\n/", "\r\n", $header );
|
||||
$header = preg_replace( "/=\r\n(\t| )+/", '= ', $header );
|
||||
$header = preg_replace( "/\r\n(\t| )+/", ' ', $header );
|
||||
|
||||
/* Parse headers */
|
||||
$headers = array();
|
||||
foreach( explode( "\r\n", trim( $header ) ) as $line )
|
||||
{
|
||||
/* Extract the value */
|
||||
$colonPosition = mb_strpos( $line, ':' );
|
||||
$headerName = mb_strtolower( mb_substr( $line, 0, $colonPosition ) );
|
||||
$headerValue = $this->_decodeHeaderValue( mb_substr( $line, $colonPosition + ( ( mb_substr( $line, $colonPosition + 1, 1 ) === ' ' ) ? 2 : 1 ) ) );
|
||||
|
||||
/* Decode the value */
|
||||
/* Save */
|
||||
if ( isset( $headers[ $headerName ] ) )
|
||||
{
|
||||
if ( \is_array( $headers[ $headerName ] ) )
|
||||
{
|
||||
$headers[ $headerName ][] = $headerValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
$headers[ $headerName ] = array( $headers[ $headerName ], $headerValue );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$headers[ $headerName ] = $headerValue;
|
||||
}
|
||||
}
|
||||
|
||||
/* Work out the content type */
|
||||
if ( isset( $headers['content-type'] ) and \is_string( $headers['content-type'] ) )
|
||||
{
|
||||
$contentType = $this->_parseStructuredHeader( $headers['content-type'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$contentType = array( 'value' => 'text/plain', 'extra' => array() );
|
||||
}
|
||||
|
||||
/* Parse body: Multipart */
|
||||
if ( \in_array( $contentType['value'], array( 'multipart/parallel', 'multipart/appledouble', 'multipart/report', 'multipart/signed', 'multipart/digest', 'multipart/alternative', 'multipart/related', 'multipart/mixed', 'application/vnd.wap.multipart.related' ) ) and isset( $contentType['extra']['boundary'] ) )
|
||||
{
|
||||
$parts = array();
|
||||
|
||||
/* Sometimes the email will start with "This is a multi-part message in MIME format" before the first part, so we need to strip that */
|
||||
$body = preg_replace( "/(.*?)(--" . preg_quote( $contentType['extra']['boundary'], '/') . ".*)$/s", "$2", $body );
|
||||
|
||||
foreach ( array_filter( preg_split( "/--" . preg_quote( $contentType['extra']['boundary'], '/') . "((?=\s)|--)/", $body ) ) as $part )
|
||||
{
|
||||
if ( trim( $part ) )
|
||||
{
|
||||
$parts[] = $this->_decodePart( $part );
|
||||
}
|
||||
}
|
||||
|
||||
return array( 'headers' => $headers, 'parts' => $parts );
|
||||
}
|
||||
|
||||
/* Parse body: Included message */
|
||||
elseif ( $contentType['value'] == 'message/rfc822' )
|
||||
{
|
||||
return array( 'headers' => $headers, 'parts' => array( $this->_decodePart( $body ) ) );
|
||||
}
|
||||
|
||||
/* Parse body: Normal */
|
||||
else
|
||||
{
|
||||
/* Work out the encoding */
|
||||
$encoding = null;
|
||||
if ( isset( $headers['content-transfer-encoding'] ) )
|
||||
{
|
||||
$encoding = trim( mb_strtolower( $headers['content-transfer-encoding'] ) );
|
||||
if ( mb_strpos( $encoding, ';' ) !== FALSE )
|
||||
{
|
||||
$encoding = trim( mb_substr( $encoding, mb_strpos( $encoding, ';' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Decode it */
|
||||
if ( $encoding == 'quoted-printable' )
|
||||
{
|
||||
$body = preg_replace( "/=\r?\n/", '', $body );
|
||||
|
||||
$body = preg_replace_callback( '/=([a-f0-9]{2})/i', function( $matches )
|
||||
{
|
||||
return \chr( hexdec( $matches[1] ) );
|
||||
}, $body );
|
||||
}
|
||||
elseif( $encoding == 'base64' )
|
||||
{
|
||||
$body = base64_decode( $body );
|
||||
}
|
||||
|
||||
/* Add it to the array */
|
||||
return array( 'headers' => $headers, 'body' => $body );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode header value
|
||||
*
|
||||
* @param string $headerValue Value
|
||||
* @return array
|
||||
*/
|
||||
protected function _decodeHeaderValue( $headerValue )
|
||||
{
|
||||
$headerValue = preg_replace( '/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $headerValue );
|
||||
while ( preg_match( '/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $headerValue, $matches ) )
|
||||
{
|
||||
$encoded = $matches[1];
|
||||
$charset = $matches[2];
|
||||
$encoding = $matches[3];
|
||||
$text = $matches[4];
|
||||
|
||||
switch ( mb_strtolower( $encoding ) )
|
||||
{
|
||||
case 'b':
|
||||
$text = base64_decode( $text );
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
$text = str_replace( '_', ' ', $text );
|
||||
preg_match_all( '/=([a-f0-9]{2})/i', $text, $matches );
|
||||
foreach( $matches[1] as $value )
|
||||
{
|
||||
$text = str_replace( '=' . $value, \chr( hexdec( $value ) ), $text );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* We need to convert the text to UTF-8 if it is not already */
|
||||
if ( mb_strtolower( $charset ) != 'utf-8' and ( in_array( mb_strtolower( $charset ), array_map( 'mb_strtolower', mb_list_encodings() ) ) ) )
|
||||
{
|
||||
$text = mb_convert_encoding( $text, 'UTF-8', mb_strtoupper( $charset ) );
|
||||
}
|
||||
|
||||
$headerValue = str_replace( $encoded, $text, $headerValue );
|
||||
}
|
||||
return $headerValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a structured header
|
||||
* i.e. foo="bar";baz="moo"
|
||||
* e.g. Content-Type: multipart/alternative; boundary=xxxxxx
|
||||
*
|
||||
* @param string $value Value
|
||||
* @return array
|
||||
*/
|
||||
protected function _parseStructuredHeader( $value )
|
||||
{
|
||||
$extra = array();
|
||||
$semiColonPosition = mb_strpos( $value, ';' );
|
||||
if ( $semiColonPosition !== false )
|
||||
{
|
||||
foreach ( explode( ';', trim( mb_substr( $value, $semiColonPosition + 1 ) ) ) as $extraPart )
|
||||
{
|
||||
$equalsPosition = mb_strpos( $extraPart, '=' );
|
||||
$extra[ trim( mb_substr( $extraPart, 0, $equalsPosition ) ) ] = trim( mb_substr( $extraPart, $equalsPosition + 1 ), " \t\n\r\0\x0B'\"" );
|
||||
}
|
||||
$value = trim( mb_substr( $value, 0, $semiColonPosition ) );
|
||||
}
|
||||
|
||||
return array( 'value' => mb_strtolower( $value ), 'extra' => $extra );
|
||||
}
|
||||
|
||||
/* !Secondary Email parsing (extract common headers, get the message body) */
|
||||
|
||||
/**
|
||||
* Parse basic headers (to, from, subject, cc)
|
||||
*
|
||||
* @param array $headers Raw headers
|
||||
* @return void
|
||||
*/
|
||||
protected function _parseBasicHeaders( $headers )
|
||||
{
|
||||
/* To */
|
||||
if ( $headers['to'] )
|
||||
{
|
||||
if ( mb_strpos( $headers['to'], ',' ) === FALSE )
|
||||
{
|
||||
$this->to = array( $headers['to'] );
|
||||
}
|
||||
/* We need to account for formats like "Doe, John (Contractor)" <a@b.com>, "Doe, Jane" <z@b.com> */
|
||||
elseif( mb_strpos( $headers['to'], '"' ) !== FALSE )
|
||||
{
|
||||
/* The regex here is looking for opening quote, anything except <, comma, anything, closing quote and then stripping the comma so that
|
||||
commas are only used to separate the actual addresses */
|
||||
$this->to = explode( ',', preg_replace( "/\"([^<]*?),(.*?)\"/", "\"$1 $2\"", $headers['to'] ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->to = explode( ',', $headers['to'] );
|
||||
}
|
||||
$this->to = array_map( array( $this, '_extractEmailAddress' ), $this->to );
|
||||
}
|
||||
|
||||
/* From */
|
||||
$this->from = $this->_extractEmailAddress( $headers['from'] );
|
||||
|
||||
/* Subject */
|
||||
$this->subject = ( (bool) trim( $headers['subject'] ) ) ? $headers['subject'] : \IPS\Member::load( $this->from, 'email' )->language()->get('incoming_email_no_subject');
|
||||
|
||||
/* CC */
|
||||
if( isset( $headers['cc'] ) )
|
||||
{
|
||||
if( \is_array( $headers['cc'] ) )
|
||||
{
|
||||
$this->cc = $headers['cc'];
|
||||
}
|
||||
/* We need to account for formats like "Doe, John (Contractor)" <a@b.com>, "Doe, Jane" <z@b.com> */
|
||||
elseif( mb_strpos( $headers['cc'], '"' ) !== FALSE )
|
||||
{
|
||||
/* The regex here is looking for opening quote, anything except <, comma, anything, closing quote and then stripping the comma so that
|
||||
commas are only used to separate the actual addresses */
|
||||
$this->cc = explode( ',', preg_replace( "/\"([^<]*?),(.*?)\"/", "\"$1 $2\"", $headers['cc'] ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->cc = explode( ",", $headers['cc'] );
|
||||
}
|
||||
$this->cc = array_map( array( $this, '_extractEmailAddress' ), $this->cc );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the email address from a "Name <email@example.com>" format
|
||||
*
|
||||
* @param string $contents The content to extract from
|
||||
* @return string
|
||||
*/
|
||||
protected function _extractEmailAddress( $contents )
|
||||
{
|
||||
if ( preg_match( "/.+? <(.+?)>/", $contents, $matches ) )
|
||||
{
|
||||
return $matches[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
return trim( trim( $contents ), '<>' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse message part
|
||||
*
|
||||
* @param array $data As returned by _decodePart
|
||||
* @return void
|
||||
*/
|
||||
protected function _parseMessagePart( $data )
|
||||
{
|
||||
/* If this part contains sub-parts, parse recursively... */
|
||||
if ( isset( $data['parts'] ) )
|
||||
{
|
||||
$this->_parseMultipartPart( $data['headers'], $data['parts'] );
|
||||
}
|
||||
|
||||
/* Otherwise just add to the body */
|
||||
elseif ( isset( $data['body'] ) )
|
||||
{
|
||||
/* If it's text which isn't an attachment, add it */
|
||||
$contentType = $this->_parseStructuredHeader( isset( $data['headers']['content-type'] ) ? $data['headers']['content-type'] : 'text/plain' );
|
||||
$contentDisposition = isset( $data['headers']['content-disposition'] ) ? $this->_parseStructuredHeader( $data['headers']['content-disposition'] ) : NULL;
|
||||
if ( mb_substr( $contentType['value'], 0, 5 ) === 'text/' and ( !$contentDisposition or $contentDisposition['value'] != 'attachment' ) )
|
||||
{
|
||||
$body = $this->_parseBodyAsHtml( $data['body'], $contentType );
|
||||
$quote = $this->_extractQuoteFromBody( $body );
|
||||
$this->message .= $body;
|
||||
$this->quote .= $quote;
|
||||
}
|
||||
|
||||
/* Otherwise make it an attachment */
|
||||
else
|
||||
{
|
||||
/* If there is no filename, which will often happen with inline disposition, create a random one */
|
||||
if( isset( $contentDisposition['extra']['filename'] ) )
|
||||
{
|
||||
$filename = $contentDisposition['extra']['filename'];
|
||||
}
|
||||
elseif ( isset( $contentType['extra']['name'] ) )
|
||||
{
|
||||
$filename = $contentType['extra']['name'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$filename = md5( mt_rand() );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
/* Save the file */
|
||||
$file = \IPS\File::create( 'core_Attachment', $filename, $data['body'] );
|
||||
|
||||
/* Add to the message */
|
||||
if ( $file->isImage() )
|
||||
{
|
||||
if ( isset( $data['headers']['content-id'] ) and $contentId = trim( $data['headers']['content-id'], '<>' ) and isset( $this->imagePlaceholders[ $contentId ] ) )
|
||||
{
|
||||
$imageTag = "<img src='<fileStore.core_Attachment>/{$file}' class='ipsImage ipsImage_thumbnailed'> ";
|
||||
$this->message = preg_replace( "/<a href=\"#image-placeholder={$this->imagePlaceholders[ $contentId ]}\" rel=\"[^\"]*\"><\/a>/", $imageTag, $this->message );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->message .= "<img src='<fileStore.core_Attachment>/{$file}' class='ipsImage ipsImage_thumbnailed'> ";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->message .= "<a class='ipsAttachLink' href='<fileStore.core_Attachment>/{$file}'>{$file->originalFilename}</a><br><br>";
|
||||
}
|
||||
|
||||
/* Save to the $attachments array and exit */
|
||||
$this->attachments[] = $file;
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'incoming_email' );
|
||||
|
||||
/* This could be a corrupt file, which we can't do anything about */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse multipart parts
|
||||
*
|
||||
* @param array $headers Raw headers for this part
|
||||
* @param array $parts Raw message sub-parts for this part
|
||||
* @return void
|
||||
*/
|
||||
protected function _parseMultipartPart( $headers, $parts )
|
||||
{
|
||||
$contentType = $this->_parseStructuredHeader( $headers['content-type'] );
|
||||
|
||||
/* multipart/alternative is the same content in different formats (usually html and plaintext) */
|
||||
if ( $contentType['value'] == 'multipart/alternative' )
|
||||
{
|
||||
$preferredPart = null;
|
||||
|
||||
foreach ( $parts as $part )
|
||||
{
|
||||
if ( $preferredPart === null )
|
||||
{
|
||||
$preferredPart = $part;
|
||||
}
|
||||
else
|
||||
{
|
||||
$preferredPartContentType = $this->_parseStructuredHeader( $preferredPart['headers']['content-type'] );
|
||||
$thisPartContentType = $this->_parseStructuredHeader( $part['headers']['content-type'] );
|
||||
|
||||
if ( $preferredPartContentType['value'] == 'text/plain' and $thisPartContentType['value'] != 'text/plain' )
|
||||
{
|
||||
$preferredPart = $part;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->_parseMessagePart( $preferredPart );
|
||||
}
|
||||
|
||||
/* Everything else (normally multipart/mixed) we'll assume to be broken into multiple parts (for example, attachments) */
|
||||
else
|
||||
{
|
||||
foreach ( $parts as $part )
|
||||
{
|
||||
$this->_parseMessagePart( $part );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Image placeholders
|
||||
*/
|
||||
protected $imagePlaceholders = array();
|
||||
|
||||
/**
|
||||
* Turn body into sanatised HTML for use within Invision Community
|
||||
*
|
||||
* @param string $body Email body
|
||||
* @param array $contentType Content-Type data (as returned by _parseStructuredHeader)
|
||||
* @return void
|
||||
*/
|
||||
protected function _parseBodyAsHtml( $body, $contentType )
|
||||
{
|
||||
/* If it's plaintext, nl2br */
|
||||
if ( $contentType['value'] == 'text/plain' )
|
||||
{
|
||||
$body = nl2br( $body );
|
||||
}
|
||||
|
||||
/* Convert to UTF-8 if possible */
|
||||
if ( isset( $contentType['extra']['charset'] ) and mb_strtolower( $contentType['extra']['charset'] ) != 'utf-8' )
|
||||
{
|
||||
if( \in_array( mb_strtolower( $contentType['extra']['charset'] ), array_map( 'mb_strtolower', mb_list_encodings() ) ) )
|
||||
{
|
||||
$body = mb_convert_encoding( $body, 'UTF-8', $contentType['extra']['charset'] );
|
||||
}
|
||||
}
|
||||
|
||||
/* Make any images which we haven't parsed yet placeholders */
|
||||
$body = preg_replace_callback( '/<img[^>]*src=[\'"]cid:(.+?)[\'"][^>]*>/', array( $this, '_replaceImageWithPlaceholder' ), $body );
|
||||
|
||||
/* Clean */
|
||||
$parser = new \IPS\Text\Parser( FALSE, NULL, \IPS\Member::load( $this->from, 'email' ), TRUE, TRUE, TRUE, function( $config ) {
|
||||
$config->set( 'HTML.TargetBlank', TRUE );
|
||||
} );
|
||||
$body = $parser->parse( $body );
|
||||
|
||||
/* Store any mapped attachments that will need a mapping stored in the individual incoming email handlers */
|
||||
$this->mappedAttachments = array_merge( $this->mappedAttachments, $parser->mappedAttachments );
|
||||
|
||||
/* Return */
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an image with a placeholder to be replaced later when it is parsed as an attachment
|
||||
*
|
||||
* @param array $matches Matches from preg_replace
|
||||
* @return string
|
||||
*/
|
||||
protected function _replaceImageWithPlaceholder( $matches )
|
||||
{
|
||||
$id = mt_rand();
|
||||
$this->imagePlaceholders[ $matches[1] ] = $id;
|
||||
return '<a href="#image-placeholder=' . $id . '" rel=""></a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract quote from message body
|
||||
*
|
||||
* @param string $body Email body (the quote will be removed from it and returned)
|
||||
* @return string
|
||||
*/
|
||||
protected function _extractQuoteFromBody( &$body )
|
||||
{
|
||||
/* Init */
|
||||
$_quote = array();
|
||||
$_body = array();
|
||||
$_seen = FALSE;
|
||||
$inQuote = FALSE;
|
||||
|
||||
/* Split into lines - It is possible the email is formatted with <p></p> and not <br> which can confuse the quote selection */
|
||||
if ( mb_stristr( $body, '</p>' ) or mb_stristr( $body, '</div>' ) )
|
||||
{
|
||||
$body = preg_replace( '#<(p|div)[^>]*?>#', '', $body );
|
||||
$body = preg_replace( '#</(p|div)>#', '<br>', $body );
|
||||
}
|
||||
$exploded = explode( "<br />", $body );
|
||||
if ( \count( $exploded ) === 1 )
|
||||
{
|
||||
$exploded = explode( '<br>', $body );
|
||||
}
|
||||
|
||||
/* Loop over each line */
|
||||
foreach ( $exploded as $k => $line )
|
||||
{
|
||||
$line = trim( $line );
|
||||
|
||||
if ( mb_substr( $line, 0, 1 ) == '>' )
|
||||
{
|
||||
$line = ltrim( $line, '> ' );
|
||||
|
||||
/* If we are just now hitting a quote line, go back one line to see if it's a "on .. wrote:" line */
|
||||
if( !$_seen )
|
||||
{
|
||||
/* Get the last 2 lines we pushed to the body block. Sometimes you have "on ... wrote:" followed by an empty line. */
|
||||
$_last = array_pop($_body);
|
||||
$_last2 = array_pop($_body);
|
||||
|
||||
$_check = ( !trim($_last) ) ? $_last2 : $_last;
|
||||
|
||||
/* If it ends with a colon push it to the quote block instead, otherwise put it back */
|
||||
if( mb_substr( trim($_check), -1 ) == ':' )
|
||||
{
|
||||
$_quote[] = $_last;
|
||||
$_quote[] = $_last2;
|
||||
}
|
||||
else
|
||||
{
|
||||
$_body[] = $_last;
|
||||
$_body[] = $_last2;
|
||||
}
|
||||
|
||||
/* Don't do this again */
|
||||
$_seen = true;
|
||||
}
|
||||
|
||||
$_quote[] = $line;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !$inQuote and preg_match( '/(\s|^)-* ?((Original)|(Forwarded)) Message:? ?-*/i', $line ) )
|
||||
{
|
||||
$line = preg_replace( '/-* ?(Begin )?((Original)|(Forwarded)) Message:? ?-*/i', '', $line );
|
||||
$inQuote = TRUE;
|
||||
}
|
||||
|
||||
if ( $inQuote )
|
||||
{
|
||||
$_quote[] = $line;
|
||||
}
|
||||
else
|
||||
{
|
||||
$_body[] = $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$quote = implode( "<br>", $_quote );
|
||||
$message = implode( "<br>", $_body );
|
||||
|
||||
/* Parse out <blockquote> tags which are typically used in HTML emails. Remember that blockquotes
|
||||
can be nested and that our own quote routine uses blockquotes, so we have to be careful. Look for data-ips* attributes to try to weed out our own. */
|
||||
if( mb_strpos( $message, '</blockquote>' ) !== FALSE )
|
||||
{
|
||||
/* First get the position of the last closing blockquote tag. The "13" here is "</blockquote>". */
|
||||
$_lastClosingBlockquote = mb_strrpos( $message, "</blockquote>" ) + 13;
|
||||
|
||||
/* Now get the position of the first opening blockquote tag */
|
||||
preg_match( '/<blockquote(?! data-ips).+?>/s', $message, $matches, PREG_OFFSET_CAPTURE );
|
||||
|
||||
if( $matches[0][1] )
|
||||
{
|
||||
/* Check for common "on x so and so wrote:" type lines preceeding this position */
|
||||
preg_match_all( '/<div([^>]+?)?>((?!<div).)*:(<br(>| \/>))*\s*<\/div>/sU', $message, $possibleHeaders, PREG_OFFSET_CAPTURE );
|
||||
if ( \count( $possibleHeaders ) )
|
||||
{
|
||||
foreach ( array_reverse( $possibleHeaders[0] ) as $header )
|
||||
{
|
||||
if ( $header[1] < $matches[0][1] and mb_strpos( mb_substr( $message, $header[1], $matches[0][1] - $header[1] ), "\n" ) === FALSE )
|
||||
{
|
||||
$matches[0][1] = $header[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preg_match( '/<div class=[\'"][^>]*?quote[^>]*?[\'"]>(.*?):(<br(>| \/>))?\s*<blockquote(?! data-ips).+?>/s', $message, $header, PREG_OFFSET_CAPTURE );
|
||||
if( !empty($header) AND $header[1][1] < $matches[0][1] )
|
||||
{
|
||||
$matches[0][1] = $header[1][1];
|
||||
}
|
||||
|
||||
/* Now take everything between these positions and move into the quoted content. substr() rather than mb_substr() because the regex we used to get the offset isn't multibyte aware */
|
||||
$quote = \substr( $message, $matches[0][1], ( $_lastClosingBlockquote - $matches[0][1] ) );
|
||||
|
||||
/* And finally, remove the quoted stuff from our email message. substr() rather than mb_substr() because the regex we used to get the offset isn't multibyte aware */
|
||||
$message = \substr( $message, 0, $matches[0][1] ) . mb_substr( $message, $_lastClosingBlockquote );
|
||||
}
|
||||
}
|
||||
|
||||
/* Make sure each section is valid HTML */
|
||||
$body = $message;
|
||||
if ( $quote )
|
||||
{
|
||||
/* Because we have already parsed the body we have to swap these back otherwise HTMLPurifier will think they're invalid
|
||||
HTML. The reparsing we do below will put them back automatically. */
|
||||
$body = str_replace( '<___base_url___>', rtrim( \IPS\Settings::i()->base_url, '/' ), $body );
|
||||
$quote = str_replace( '<___base_url___>', rtrim( \IPS\Settings::i()->base_url, '/' ), $quote );
|
||||
|
||||
/* Parse */
|
||||
$body = \IPS\Text\Parser::parseStatic( $body, FALSE, NULL, \IPS\Member::load( $this->from, 'email' ) );
|
||||
$quote = \IPS\Text\Parser::parseStatic( $quote, FALSE, NULL, \IPS\Member::load( $this->from, 'email' ) );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $quote;
|
||||
}
|
||||
|
||||
/* !Routing */
|
||||
|
||||
/**
|
||||
* Route the email and pass off to the appropriate handler
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function route()
|
||||
{
|
||||
/* Ignore auto-responder messages */
|
||||
if( $this->isAutoreply() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Initialize some vars */
|
||||
$routed = FALSE;
|
||||
|
||||
/* Get our filter rules from the database */
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_incoming_emails' ) as $row )
|
||||
{
|
||||
/* Reset some vars */
|
||||
$analyze = NULL;
|
||||
$match = FALSE;
|
||||
|
||||
/* Field to check */
|
||||
switch ( $row['rule_criteria_field'] )
|
||||
{
|
||||
case 'to':
|
||||
$analyze = implode( ',', $this->to );
|
||||
break;
|
||||
|
||||
case 'from':
|
||||
$analyze = $this->from;
|
||||
break;
|
||||
|
||||
case 'sbjt':
|
||||
$analyze = $this->subject;
|
||||
break;
|
||||
|
||||
case 'body':
|
||||
$analyze = $this->message;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Now check if we match the supplied rule */
|
||||
switch ( $row['rule_criteria_type'] )
|
||||
{
|
||||
case 'ctns':
|
||||
$match = (bool) ( mb_strpos( $analyze, $row['rule_criteria_value'] ) !== FALSE );
|
||||
break;
|
||||
|
||||
case 'eqls':
|
||||
if ( mb_strpos( $analyze, ',' ) !== FALSE )
|
||||
{
|
||||
$match = (bool) \in_array( $analyze, explode( ',', $analyze ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$match = (bool) ( $analyze == $row['rule_criteria_value'] );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'regx':
|
||||
$match = (bool) ( preg_match( "/{$row['rule_criteria_value']}/", $analyze ) == 1 );
|
||||
break;
|
||||
}
|
||||
|
||||
/* Do we have a match? */
|
||||
if ( $match === TRUE )
|
||||
{
|
||||
$routed = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If we are still here, check each app to see if it wants to handle this unrouted incoming email */
|
||||
if ( !$routed )
|
||||
{
|
||||
/* Loop over all apps that have an incoming email extension */
|
||||
foreach ( \IPS\Application::appsWithExtension( 'core', 'IncomingEmail', FALSE ) as $dir => $app )
|
||||
{
|
||||
/* Get all IncomingEmail extension classes for the app */
|
||||
$extensions = $app->extensions( 'core', 'IncomingEmail' );
|
||||
|
||||
if( \count( $extensions ) )
|
||||
{
|
||||
/* Loop over the extensions */
|
||||
foreach( $extensions as $_instance )
|
||||
{
|
||||
/* And if it returns true, the unrouted email has now been handled. We can break. */
|
||||
if( $routed = $_instance->process( $this ) )
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we are still here, send an "unrouted email" email to the sender */
|
||||
if ( !$routed )
|
||||
{
|
||||
if ( \IPS\Email::hasTemplate( 'core', 'unrouted' ) )
|
||||
{
|
||||
\IPS\Email::buildFromTemplate( 'core', 'unrouted', $this, \IPS\Email::TYPE_TRANSACTIONAL )->send( $this->from );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this an auto-reply? Try to detect to prevent auto-reply loops.
|
||||
*
|
||||
* @return bool
|
||||
* @see https://github.com/opennorth/multi_mail/wiki/Detecting-autoresponders
|
||||
*/
|
||||
protected function isAutoreply()
|
||||
{
|
||||
/* RFC http://tools.ietf.org/html/rfc3834 */
|
||||
if( !empty( $this->headers['auto-submitted'] ) AND mb_strtolower( $this->headers['auto-submitted'] ) != 'no' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* If any of these headers are present with any values, ignore the email */
|
||||
if( !empty( $this->headers['x-autorespond'] ) OR
|
||||
!empty( $this->headers['x-autoreply'] ) OR
|
||||
!empty( $this->headers['x-autoreply-From'] ) OR
|
||||
!empty( $this->headers['x-mail-autoreply'] )
|
||||
)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Now we check for a null return-path (which is considered the "proper" way to prevent auto-responses) */
|
||||
if( !empty( $this->headers['return-path'] ) AND $this->headers['return-path'] == '<>' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Now check for specific headers with specific values */
|
||||
if( !empty( $this->headers['x-autogenerated'] ) AND \in_array( mb_strtolower( $this->headers['x-autogenerated'] ), array( 'forward', 'group', 'letter', 'mirror', 'redirect', 'reply' ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if( !empty( $this->headers['precedence'] ) AND \in_array( mb_strtolower( $this->headers['precedence'] ), array( 'auto_reply', 'list', 'bulk' ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if( !empty( $this->headers['x-precedence'] ) AND \in_array( mb_strtolower( $this->headers['x-precedence'] ), array( 'auto_reply', 'list', 'bulk' ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if( !empty( $this->headers['x-fc-machinegenerated'] ) AND mb_strtolower( $this->headers['x-fc-machinegenerated'] ) == 'true' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if( !empty( $this->headers['x-post-messageclass'] ) AND mb_strtolower( $this->headers['x-post-messageclass'] ) == '9; autoresponder' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if ( !empty( $this->headers['delivered-to'] ) AND \is_array( $this->headers['delivered-to'] ) )
|
||||
{
|
||||
foreach( $this->headers['delivered-to'] AS $deliveredTo )
|
||||
{
|
||||
if ( mb_strtolower( $deliveredTo ) == 'autoresponder' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( !empty( $this->headers['delivered-to'] ) AND \is_string( $this->headers['delivered-to'] ) AND mb_strtolower( $this->headers['delivered-to'] ) == 'autoresponder' )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* And finally, some basic checks on the subject line */
|
||||
if( mb_stripos( $this->headers['subject'], "out of office: " ) === 0 OR
|
||||
mb_stripos( $this->headers['subject'], "out of office autoreply:" ) === 0 OR
|
||||
mb_stripos( $this->headers['subject'], "automatic reply: " ) === 0 OR
|
||||
mb_strtolower( $this->headers['subject'] ) === "out of office"
|
||||
)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief POP/IMAP Incoming Email Handler
|
||||
* @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 21 December 2015
|
||||
*/
|
||||
|
||||
namespace IPS\Email\Incoming;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP Email Class
|
||||
*/
|
||||
class _PopImap
|
||||
{
|
||||
/**
|
||||
* @brief File pointer
|
||||
*/
|
||||
protected $fh;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $server Server name (e.g. pop.example.com)
|
||||
* @param bool $sslRequired If SSL is required
|
||||
* @param int $port Port
|
||||
* @param string $username Username
|
||||
* @param string $password Password
|
||||
* @return void
|
||||
* @throws \IPS\Email\Incoming\PopImapException
|
||||
*/
|
||||
public function __construct( $server, $sslRequired, $port, $username, $password )
|
||||
{
|
||||
/* Connect */
|
||||
$errorNumber = 0;
|
||||
$errorMessage = '';
|
||||
$this->fh = @fsockopen( ( $sslRequired ? 'ssl://' : '' ) . $server, $port, $errorNumber, $errorMessage );
|
||||
if ( $this->fh === FALSE )
|
||||
{
|
||||
throw new PopImapException( 'pop3_cant_connect' );
|
||||
}
|
||||
|
||||
/* Set stream preferences */
|
||||
stream_set_timeout( $this->fh, \IPS\DEFAULT_REQUEST_TIMEOUT );
|
||||
stream_set_blocking( $this->fh, TRUE );
|
||||
|
||||
/* Get initial response */
|
||||
try
|
||||
{
|
||||
$response = $this->_getLine();
|
||||
}
|
||||
catch ( PopImapException $e )
|
||||
{
|
||||
throw new PopImapException( 'pop3_connect_err' );
|
||||
}
|
||||
|
||||
/* Log In */
|
||||
try
|
||||
{
|
||||
$this->_putLine( "USER {$username}" );
|
||||
$this->_putLine( "PASS {$password}" );
|
||||
}
|
||||
catch ( PopImapException $e )
|
||||
{
|
||||
throw new PopImapException( 'pop3_login_err' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of emails in inbox
|
||||
*
|
||||
* @return int
|
||||
* @throws \IPS\Email\Incoming\PopImapException
|
||||
*/
|
||||
public function emailsInInbox()
|
||||
{
|
||||
$stats = explode( ' ', $this->_putLine( 'STAT' ) );
|
||||
return (int) $stats[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email
|
||||
*
|
||||
* @param int $i The email to fetch
|
||||
* @return string
|
||||
* @throws \IPS\Email\Incoming\PopImapException
|
||||
*/
|
||||
public function getEmail( $i )
|
||||
{
|
||||
$this->_putLine( "RETR {$i}" );
|
||||
|
||||
$message = '';
|
||||
while ( $line = $this->_getLine( FALSE ) and !preg_match( "/^\.\r\n/", $line ) )
|
||||
{
|
||||
$message .= $line;
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete email
|
||||
*
|
||||
* @param int $i The email to delete
|
||||
* @return string
|
||||
* @throws \IPS\Email\Incoming\PopImapException
|
||||
*/
|
||||
public function deleteEmail( $i )
|
||||
{
|
||||
$this->_putLine( "DELE {$i}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Line
|
||||
*
|
||||
* @param bool $expectOkay The response should start with "+OK"
|
||||
* @return string
|
||||
*/
|
||||
public function _getLine( $expectOkay = TRUE )
|
||||
{
|
||||
$response = fgets( $this->fh, 512 );
|
||||
|
||||
$status = socket_get_status( $this->fh );
|
||||
if ( $status['timed_out'] )
|
||||
{
|
||||
throw new \IPS\Email\Incoming\PopImapException( 'TIMEOUT' );
|
||||
}
|
||||
|
||||
if ( $expectOkay and mb_substr( $response, 0, 3 ) !== '+OK' )
|
||||
{
|
||||
throw new \IPS\Email\Incoming\PopImapException( 'UNEXPECTED_RESPONSE: ' . $response );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put Line
|
||||
*
|
||||
* @param string $command Command to send
|
||||
* @param bool $expectOkay The response should start with "+OK"
|
||||
* @return string The response
|
||||
*/
|
||||
public function _putLine( $command, $expectOkay = TRUE )
|
||||
{
|
||||
if ( @\fwrite( $this->fh, $command . "\r\n" ) === FALSE )
|
||||
{
|
||||
throw new \IPS\Email\Incoming\PopImapException( 'COULD_NOT_WRITE' );
|
||||
}
|
||||
|
||||
return $this->_getLine( $expectOkay );
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->_putLine( "QUIT", FALSE );
|
||||
fclose( $this->fh );
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief POP/IMAP Exception 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 21 December 2015
|
||||
*/
|
||||
|
||||
namespace IPS\Email\Incoming;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP/IMAP Exception class
|
||||
*/
|
||||
class _PopImapException extends \Exception
|
||||
{
|
||||
|
||||
}
|
||||
Whitespace-only changes.
@@ -11,29 +11,34 @@
|
||||
namespace IPS\Email\Outgoing;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Email;
|
||||
use function defined;
|
||||
use function file_put_contents;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug Email Class
|
||||
*/
|
||||
class _Debug extends \IPS\Email
|
||||
class Debug extends Email
|
||||
{
|
||||
/**
|
||||
* @brief Debug path to write the email files
|
||||
*/
|
||||
protected $debugPath = '';
|
||||
protected string $debugPath = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $debugPath Debug path
|
||||
* @param string $debugPath Debug path
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $debugPath )
|
||||
public function __construct( string $debugPath )
|
||||
{
|
||||
$this->debugPath = $debugPath;
|
||||
}
|
||||
@@ -42,43 +47,27 @@ class _Debug extends \IPS\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
|
||||
* @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
|
||||
* @throws Exception
|
||||
*/
|
||||
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
|
||||
public function _send( mixed $to, mixed $cc=array(), mixed $bcc=array(), mixed $fromEmail = NULL, mixed $fromName = NULL, array $additionalHeaders = array() ) : void
|
||||
{
|
||||
if( !is_dir( $this->debugPath ) )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( 'no_path_email_debug', 1, NULL, array( $this->debugPath ) );
|
||||
throw new Exception( 'no_path_email_debug', 1, NULL, array( $this->debugPath ) );
|
||||
}
|
||||
|
||||
$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 ) )
|
||||
if( !@file_put_contents( rtrim( $this->debugPath, '/' ) . '/' . $filename, $fullEmailContents ) )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( 'no_write_email_debug', 2, NULL, array( rtrim( $this->debugPath, '/' ) . '/' . $filename ) );
|
||||
throw new Exception( 'no_write_email_debug', 2, NULL, array( rtrim( $this->debugPath, '/' ) . '/' . $filename ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this email class usable?
|
||||
*
|
||||
* @param string $type Email Type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUsable( string $type ): bool
|
||||
{
|
||||
if( \IPS\EMAIL_DEBUG_PATH )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -11,38 +11,43 @@
|
||||
namespace IPS\Email\Outgoing;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Member;
|
||||
use RuntimeException;
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception class for email errors
|
||||
*/
|
||||
class _Exception extends \RuntimeException
|
||||
class Exception extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* @brief Extra details for log
|
||||
*/
|
||||
public $extraDetails = array();
|
||||
public $messageKey = NULL;
|
||||
public ?array $extraDetails = array();
|
||||
public ?string $messageKey = NULL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $message Error message
|
||||
* @param int $code Error Code
|
||||
* @param \Exception|NULL $previous Previous Exception
|
||||
* @param array|NULL $extra Extra details for log
|
||||
* @return void
|
||||
* @param string|null $message Error message
|
||||
* @param int $code Error Code
|
||||
* @param Exception|NULL $previous Previous Exception
|
||||
* @param array|null $extra Extra details for log
|
||||
*/
|
||||
public function __construct( $message = null, $code = 0, $previous = null, $extra=NULL )
|
||||
public function __construct( string $message = null, int $code = 0, \Exception|null $previous = null, array $extra=NULL )
|
||||
{
|
||||
/* Store these for the extraLogData() method */
|
||||
$this->extraDetails = $extra;
|
||||
$this->messageKey = $message;
|
||||
$message = \IPS\Member::loggedIn()->language()->addToStack( $message, FALSE, array( 'sprintf' => $extra ) );
|
||||
|
||||
$message = Member::loggedIn()->language()->addToStack( $message, FALSE, array( 'sprintf' => $extra ) );
|
||||
|
||||
return parent::__construct( $message, $code, $previous );
|
||||
}
|
||||
|
||||
@@ -10,38 +10,43 @@
|
||||
|
||||
namespace IPS\Email\Outgoing;
|
||||
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Settings;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Email;
|
||||
use IPS\IPS;
|
||||
use IPS\Settings;
|
||||
use function defined;
|
||||
use function function_exists;
|
||||
use function in_array;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP Email Class
|
||||
*/
|
||||
class _PHP extends \IPS\Email
|
||||
class Php extends 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
|
||||
* @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
|
||||
* @throws Exception
|
||||
*/
|
||||
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
|
||||
public function _send( mixed $to, mixed $cc=array(), mixed $bcc=array(), mixed $fromEmail = NULL, mixed $fromName = NULL, array $additionalHeaders = array() ) : void
|
||||
{
|
||||
if( !\function_exists( 'mail' ) )
|
||||
if( !function_exists( 'mail' ) )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( 'email_test_mailfunction_disabled' );
|
||||
throw new Exception( 'email_test_mailfunction_disabled' );
|
||||
}
|
||||
|
||||
$boundary = "--==_mimepart_" . md5( mt_rand() );
|
||||
@@ -50,7 +55,7 @@ class _PHP extends \IPS\Email
|
||||
$headers = array();
|
||||
foreach( $this->_compileHeaders( $subject, $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders, $boundary ) as $k => $v )
|
||||
{
|
||||
if ( !\in_array( $k, array( 'To', 'Subject' ) ) )
|
||||
if ( !in_array( $k, array( 'To', 'Subject' ) ) )
|
||||
{
|
||||
$headers[] = "{$k}: {$v}";
|
||||
}
|
||||
@@ -60,50 +65,20 @@ class _PHP extends \IPS\Email
|
||||
{
|
||||
if ( !mail( $this->_parseRecipients( $to, TRUE ), static::encodeHeader( $subject ), $this->_compileMessage( static::_getMemberFromRecipients( $to ), $boundary, "\r\n", 68 ), implode( "\r\n", $headers ), Settings::i()->php_mail_extra ) )
|
||||
{
|
||||
if ( $error = \IPS\IPS::$lastError )
|
||||
if ( $error = IPS::$lastError )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( $error->getMessage(), $error->getCode() );
|
||||
throw new Exception( $error->getMessage(), $error->getCode() );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If $error is null, mail() is probably disabled */
|
||||
throw new \IPS\Email\Outgoing\Exception( 'email_test_mailfunction_disabled' );
|
||||
throw new Exception( 'email_test_mailfunction_disabled' );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
catch ( Exception $e )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( $e->getMessage() );
|
||||
throw new Exception( $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this email class usable?
|
||||
*
|
||||
* @param string $type Email Type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUsable( string $type ): bool
|
||||
{
|
||||
if( !\IPS\CIC )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form fields for email handler
|
||||
* These will automatically toggle on when the email handler is selected to be used
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function form(): array
|
||||
{
|
||||
$form = [];
|
||||
$form['php_mail_extra'] = new Text( 'php_mail_extra', Settings::i()->php_mail_extra, FALSE, [], NULL, NULL, NULL, 'php_mail_extra' );
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Postmark 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 21 November 2024
|
||||
*/
|
||||
|
||||
namespace IPS\Email\Outgoing;
|
||||
|
||||
use Cassandra\Set;
|
||||
use IPS\core\Advertisement;
|
||||
use IPS\Db;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
use IPS\Settings;
|
||||
use const IPS\LONG_REQUEST_TIMEOUT;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Postmark Email Class
|
||||
*/
|
||||
class _Postmark extends \IPS\Email
|
||||
{
|
||||
/* !Configuration */
|
||||
|
||||
/**
|
||||
* @brief The number of emails that can be sent in one "go"
|
||||
*/
|
||||
const MAX_EMAILS_PER_GO = 500; // Postmark can accept 500 messages per batch call
|
||||
|
||||
/**
|
||||
* @brief API Key
|
||||
*/
|
||||
protected string $apiKey;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $apiKey API Key
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( string $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() )
|
||||
{
|
||||
$emailData = $this->_constructEmailRequest( $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders );
|
||||
|
||||
/* Send */
|
||||
$response = $this->api( 'email', $emailData );
|
||||
if ( !empty( $response['ErrorCode'] ) )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( $response['Message'], $response['ErrorCode'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this email class usable?
|
||||
*
|
||||
* @param string $type Email Type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUsable( string $type ): bool
|
||||
{
|
||||
if( ( new \IPS\core\extensions\core\CommunityEnhancements\Postmark() )->enabled )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Lang|NULL $language The language the email content should be in
|
||||
* @return int Number of successful sends
|
||||
*/
|
||||
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = [], Lang $language = NULL )
|
||||
{
|
||||
/* Get the current locale, and then set the language's locale so datetime formatting in templates is correct for this language */
|
||||
$currentLocale = setlocale( LC_ALL, '0' );
|
||||
$language->setLocale();
|
||||
|
||||
$emailData = $emailDataForLog = [];
|
||||
|
||||
/* 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 )
|
||||
{
|
||||
/* 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;
|
||||
|
||||
$e = $this->_constructEmailRequest( to: $address, fromEmail: $fromEmail, fromName: $fromName, language: $language );
|
||||
|
||||
foreach ( $vars as $k => $v )
|
||||
{
|
||||
$language->parseEmail( $v );
|
||||
|
||||
$e['HtmlBody'] = str_replace( "*|{$k}|*", htmlspecialchars( $v, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', TRUE ), $e['HtmlBody'] );
|
||||
$e['TextBody'] = str_replace( "*|{$k}|*", $v, $e['TextBody'] );
|
||||
$e['Subject'] = str_replace( "*|{$k}|*", $v, $e['Subject'] );
|
||||
|
||||
foreach ( $e['Headers'] as $i => $header )
|
||||
{
|
||||
$e['Headers'][ $i ]['Value'] = str_replace( "*|{$k}|*", $v, $header['Value'] );
|
||||
}
|
||||
}
|
||||
|
||||
$emailDataForLog[ $address ] = [
|
||||
'Subject' => $e['Subject'],
|
||||
'HtmlBody' => $e['HtmlBody'],
|
||||
];
|
||||
$emailData[] = $e;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
/* Send */
|
||||
$response = $this->api( 'email/batch', $emailData );
|
||||
}
|
||||
catch( \JsonException | \IPS\Email\Outgoing\Exception $e )
|
||||
{
|
||||
$first = array_shift( $emailData );
|
||||
Db::i()->insert( 'core_mail_error_logs', [
|
||||
'mlog_date' => time(),
|
||||
'mlog_to' => json_encode( array_keys( $emailDataForLog ) ),
|
||||
'mlog_from' => $fromEmail ?: Settings::i()->email_out,
|
||||
'mlog_subject' => $first['Subject'],
|
||||
'mlog_content' => $first['HtmlBody'],
|
||||
'mlog_resend_data' => json_encode( [ 'type' => $this->type ] ),
|
||||
'mlog_msg' => json_encode( [ 'message' => $e->getMessage() ] ),
|
||||
'mlog_smtp_log' => NULL
|
||||
] );
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$errorCount = 0;
|
||||
foreach( $response as $k => $result )
|
||||
{
|
||||
if( !empty( $result['ErrorCode'] ) )
|
||||
{
|
||||
$errorCount++;
|
||||
Db::i()->insert( 'core_mail_error_logs', array(
|
||||
'mlog_date' => time(),
|
||||
'mlog_to' => $emailData[ $k ]['To'],
|
||||
'mlog_from' => $fromEmail ?: Settings::i()->email_out,
|
||||
'mlog_subject' => $emailData[ $k ]['Subject'],
|
||||
'mlog_content' => $emailData[ $k ]['HtmlBody'],
|
||||
'mlog_resend_data' => json_encode( [ 'type' => $this->type ] ),
|
||||
'mlog_msg' => json_encode( [ 'code' => $result['ErrorCode'], 'message' => $result['Message'] ] ),
|
||||
'mlog_smtp_log' => NULL
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
$successCount = ( \count( $recipients ) - $errorCount );
|
||||
|
||||
/* Update ad impression count */
|
||||
Advertisement::updateEmailImpressions( $successCount );
|
||||
|
||||
/* Now restore the locale we started with */
|
||||
Lang::restoreLocale( $currentLocale );
|
||||
|
||||
return $successCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct email array for Postmar
|
||||
*
|
||||
* @param Member|string|array $to Recipient addresses
|
||||
* @param string|array $cc CC addresses
|
||||
* @param string|array $bcc BCC Addresses
|
||||
* @param string|null $fromEmail From Email Address
|
||||
* @param string|null $fromName From Name
|
||||
* @param array|null $additionalHeaders Additional headers
|
||||
* @param Lang|NULL $language
|
||||
* @return array
|
||||
*/
|
||||
protected function _constructEmailRequest( Member|string|array $to, string|array $cc=[], string|array $bcc=[], ?string $fromEmail=NULL, ?string $fromName=NULL, ?array $additionalHeaders=array(), Lang $language=NULL ): array
|
||||
{
|
||||
$fromName = $fromName ?: Settings::i()->board_name;
|
||||
$fromEmail = $fromEmail ?: Settings::i()->email_out;
|
||||
|
||||
/* Add the recipients */
|
||||
$sendTo = $sendCc = $sendBcc = [];
|
||||
foreach ( [ 'to', 'cc', 'bcc' ] as $type )
|
||||
{
|
||||
$sendVar = 'send' . ucfirst( $type );
|
||||
if ( \is_array( $$type ) )
|
||||
{
|
||||
foreach ( $$type as $recipient )
|
||||
{
|
||||
if ( $recipient instanceof Member )
|
||||
{
|
||||
$$sendVar[] = $recipient->email;
|
||||
}
|
||||
else
|
||||
{
|
||||
$$sendVar[] = $recipient;
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ( $$type )
|
||||
{
|
||||
$recipient = $$type;
|
||||
if ( $recipient instanceof Member )
|
||||
{
|
||||
$$sendVar[] = $recipient->email;
|
||||
}
|
||||
else
|
||||
{
|
||||
$$sendVar[] = $recipient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$email = [
|
||||
'From' => "{$fromName} <{$fromEmail}>",
|
||||
'To' => implode( ',', $sendTo ),
|
||||
'Cc' => implode( ',', $sendCc ),
|
||||
'Bcc' => implode( ',', $sendBcc ),
|
||||
'Subject' => $this->compileSubject( static::_getMemberFromRecipients( $to ), $language ),
|
||||
'HtmlBody' => $this->compileContent( 'html', static::_getMemberFromRecipients( $to ), $language ),
|
||||
'TextBody' => $this->compileContent( 'plaintext', static::_getMemberFromRecipients( $to ), $language ),
|
||||
'Headers' => [],
|
||||
'MessageStream' => $this->getMessageStreamId( $this->type ),
|
||||
'TrackOpens' => Settings::i()->postmark_track_opens,
|
||||
'TrackLinks' => Settings::i()->postmark_track_opens ? 'HtmlAndText' : 'None'
|
||||
];
|
||||
|
||||
if( $additionalHeaders )
|
||||
{
|
||||
foreach( $additionalHeaders as $id => $header )
|
||||
{
|
||||
$email['Headers'][] = [
|
||||
'Name' => $id,
|
||||
'Value' => $header,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API call
|
||||
*
|
||||
* @param string $method Method
|
||||
* @param array $args Arguments
|
||||
* @throws \IPS\Email\Outgoing\Exception Indicates an invalid JSON response or HTTP error
|
||||
* @return array|null
|
||||
*/
|
||||
public function api( $method, $args=NULL )
|
||||
{
|
||||
$request = Url::external( 'https://api.postmarkapp.com/' . $method )
|
||||
->request( LONG_REQUEST_TIMEOUT )
|
||||
->setHeaders( [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Postmark-Server-Token' => $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 ( \RuntimeException | \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array|null Unpacked message stream config
|
||||
*/
|
||||
protected static null|array $messageStreams = null;
|
||||
|
||||
/**
|
||||
* Get Message Stream ID based on config
|
||||
*
|
||||
* @param string $type TYPE_* constant
|
||||
* @return string
|
||||
*/
|
||||
public function getMessageStreamId( string $type ): string
|
||||
{
|
||||
if( self::$messageStreams === null )
|
||||
{
|
||||
$setting = json_decode( Settings::i()->postmark_streams, TRUE );
|
||||
self::$messageStreams = [
|
||||
static::TYPE_TRANSACTIONAL => $setting['transactional'],
|
||||
static::TYPE_LIST => $setting['bulk'],
|
||||
static::TYPE_BULK => $setting['bulk']
|
||||
];
|
||||
}
|
||||
|
||||
return self::$messageStreams[ $type ];
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,32 @@
|
||||
|
||||
namespace IPS\Email\Outgoing;
|
||||
|
||||
use IPS\Settings;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\core\Advertisement;
|
||||
use IPS\Db;
|
||||
use IPS\Email;
|
||||
use IPS\Http\Request\Exception;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
use IPS\Settings;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function is_array;
|
||||
use const IPS\LONG_REQUEST_TIMEOUT;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* SendGrid Email Class
|
||||
*/
|
||||
class _SendGrid extends \IPS\Email
|
||||
class SendGrid extends Email
|
||||
{
|
||||
/* !Configuration */
|
||||
|
||||
@@ -34,15 +47,15 @@ class _SendGrid extends \IPS\Email
|
||||
/**
|
||||
* @brief API Key
|
||||
*/
|
||||
protected $apiKey;
|
||||
protected string $apiKey;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $apiKey API Key
|
||||
* @param string $apiKey API Key
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $apiKey )
|
||||
public function __construct( string $apiKey )
|
||||
{
|
||||
$this->apiKey = $apiKey;
|
||||
}
|
||||
@@ -51,15 +64,15 @@ class _SendGrid extends \IPS\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
|
||||
* @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
|
||||
* @throws Exception
|
||||
*/
|
||||
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
|
||||
public function _send( mixed $to, mixed $cc=array(), mixed $bcc=array(), mixed $fromEmail = NULL, mixed $fromName = NULL, array $additionalHeaders = array() ) : void
|
||||
{
|
||||
/* Initiate the request */
|
||||
$request = $this->_initRequest( $fromEmail, $fromName );
|
||||
@@ -80,11 +93,11 @@ class _SendGrid extends \IPS\Email
|
||||
/* Add the recipients */
|
||||
foreach ( array( 'to', 'cc', 'bcc' ) as $type )
|
||||
{
|
||||
if ( \is_array( $$type ) )
|
||||
if ( is_array( $$type ) )
|
||||
{
|
||||
foreach ( $$type as $recipient )
|
||||
{
|
||||
if ( $recipient instanceof \IPS\Member )
|
||||
if ( $recipient instanceof Member )
|
||||
{
|
||||
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient->email, 'name' => $recipient->name );
|
||||
}
|
||||
@@ -97,7 +110,7 @@ class _SendGrid extends \IPS\Email
|
||||
elseif ( $$type )
|
||||
{
|
||||
$recipient = $$type;
|
||||
if ( $recipient instanceof \IPS\Member )
|
||||
if ( $recipient instanceof Member )
|
||||
{
|
||||
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient->email, 'name' => $recipient->name );
|
||||
}
|
||||
@@ -115,50 +128,29 @@ class _SendGrid extends \IPS\Email
|
||||
$response = $this->_api( 'mail/send', $request );
|
||||
if ( isset( $response['errors'] ) )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( $response['errors'][0]['message'] );
|
||||
throw new Exception( $response['errors'][0]['message'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this email class usable?
|
||||
*
|
||||
* @param string $type Email Type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUsable( string $type ): bool
|
||||
{
|
||||
if( ( new \IPS\core\extensions\core\CommunityEnhancements\Sendgrid() )->enabled )
|
||||
{
|
||||
return match( true )
|
||||
{
|
||||
( $type == \IPS\Email::TYPE_TRANSACTIONAL OR $type == \IPS\Email::TYPE_LIST ) AND Settings::i()->sendgrid_use_for == 2 => true,
|
||||
$type == \IPS\Email::TYPE_BULK AND (int) Settings::i()->sendgrid_use_for > 0 => true,
|
||||
default => false
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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|NULL $language The language the email content should be in
|
||||
* @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 Lang|NULL $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 = NULL )
|
||||
public function mergeAndSend( array $recipients, mixed $fromEmail = NULL, mixed $fromName = NULL, array $additionalHeaders = array(), Lang $language = NULL ): int
|
||||
{
|
||||
/* 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 = preg_replace( '/\*\|(.+?)\|\*/', '*|$1_plain|*', $this->compileContent( 'plaintext', FALSE, $language ) );
|
||||
$htmlContent = $this->compileContent( 'html', null, $language );
|
||||
$plaintextContent = preg_replace( '/\*\|(.+?)\|\*/', '*|$1_plain|*', $this->compileContent( 'plaintext', null, $language ) );
|
||||
$request['subject'] = preg_replace( '/\*\|(.+?)\|\*/', '*|$1_plain|*', $subject );
|
||||
$request['content'] = array(
|
||||
array(
|
||||
@@ -182,7 +174,7 @@ class _SendGrid extends \IPS\Email
|
||||
{
|
||||
$language->parseEmail( $v );
|
||||
$finalSubstitutions["*|{$k}_plain|*"] = $v;
|
||||
$finalSubstitutions["*|{$k}|*"] = htmlspecialchars( $v, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', TRUE );
|
||||
$finalSubstitutions["*|{$k}|*"] = htmlspecialchars( $v, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8' );
|
||||
}
|
||||
|
||||
$request['personalizations'][] = array(
|
||||
@@ -195,16 +187,16 @@ class _SendGrid extends \IPS\Email
|
||||
$request = $this->_modifyRequestDataWithHeaders( $request, $additionalHeaders );
|
||||
|
||||
/* Log emails sent */
|
||||
$this->_trackStatistics( \count( $addresses ) );
|
||||
$this->_trackStatistics( count( $addresses ) );
|
||||
|
||||
/* Send */
|
||||
try
|
||||
{
|
||||
$response = $this->_api( 'mail/send', $request );
|
||||
}
|
||||
catch( \IPS\Email\Outgoing\Exception $e )
|
||||
catch(Exception $e )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
|
||||
Db::i()->insert( 'core_mail_error_logs', array(
|
||||
'mlog_date' => time(),
|
||||
'mlog_to' => json_encode( $addresses ),
|
||||
'mlog_from' => $fromEmail ?: Settings::i()->email_out,
|
||||
@@ -221,8 +213,8 @@ class _SendGrid extends \IPS\Email
|
||||
$errorcount = 0;
|
||||
if ( isset( $response['errors'] ) )
|
||||
{
|
||||
$errorcount = \count( $response['errors'] );
|
||||
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
|
||||
$errorcount = count( $response['errors'] );
|
||||
Db::i()->insert( 'core_mail_error_logs', array(
|
||||
'mlog_date' => time(),
|
||||
'mlog_to' => json_encode( $addresses ),
|
||||
'mlog_from' => $fromEmail ?: Settings::i()->email_out,
|
||||
@@ -234,10 +226,10 @@ class _SendGrid extends \IPS\Email
|
||||
) );
|
||||
}
|
||||
|
||||
$successCount = ( \count( $recipients ) - $errorcount );
|
||||
$successCount = ( count( $recipients ) - $errorcount );
|
||||
|
||||
/* Update ad impression count */
|
||||
\IPS\core\Advertisement::updateEmailImpressions( $successCount );
|
||||
Advertisement::updateEmailImpressions( $successCount );
|
||||
|
||||
return $successCount;
|
||||
}
|
||||
@@ -245,11 +237,11 @@ class _SendGrid extends \IPS\Email
|
||||
/**
|
||||
* Create a request
|
||||
*
|
||||
* @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 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
|
||||
* @return array
|
||||
*/
|
||||
protected function _initRequest( $fromEmail = NULL, $fromName = NULL )
|
||||
protected function _initRequest( mixed $fromEmail = NULL, mixed $fromName = NULL ): array
|
||||
{
|
||||
$request = array(
|
||||
'personalizations' => array(),
|
||||
@@ -279,12 +271,12 @@ class _SendGrid extends \IPS\Email
|
||||
/**
|
||||
* 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
|
||||
* @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() )
|
||||
protected function _modifyRequestDataWithHeaders( array $request, array $additionalHeaders = array(), array $allowedTags = array() ): array
|
||||
{
|
||||
/* Do we have a Reply-To? */
|
||||
if ( isset( $additionalHeaders['Reply-To'] ) )
|
||||
@@ -297,7 +289,7 @@ class _SendGrid extends \IPS\Email
|
||||
|
||||
if ( $matches[1] )
|
||||
{
|
||||
if ( preg_match( '/^\=\?UTF\-8\?B\?(.+?)\?\=$/i', trim( $matches[1] ), $_matches ) )
|
||||
if ( preg_match( '/^=\?UTF-8\?B\?(.+?)\?=$/i', trim( $matches[1] ), $_matches ) )
|
||||
{
|
||||
$request['reply_to']['name'] = base64_decode( $_matches[1] );
|
||||
}
|
||||
@@ -319,7 +311,7 @@ class _SendGrid extends \IPS\Email
|
||||
unset( $additionalHeaders['To'] );
|
||||
unset( $additionalHeaders['CC'] );
|
||||
unset( $additionalHeaders['BCC'] );
|
||||
if ( \count( $additionalHeaders ) )
|
||||
if ( count( $additionalHeaders ) )
|
||||
{
|
||||
$request['headers'] = $additionalHeaders;
|
||||
}
|
||||
@@ -331,15 +323,15 @@ class _SendGrid extends \IPS\Email
|
||||
/**
|
||||
* Make API call
|
||||
*
|
||||
* @param string $method Method
|
||||
* @param array $args Arguments
|
||||
* @throws \IPS\Email\Outgoing\Exception Indicates an invalid JSON response or HTTP error
|
||||
* @return array|null
|
||||
* @param string $method Method
|
||||
* @param array|null $args Arguments
|
||||
* @return array|null
|
||||
*@throws Exception Indicates an invalid JSON response or HTTP error
|
||||
*/
|
||||
protected function _api( $method, $args=NULL )
|
||||
protected function _api(string $method, array $args=NULL ): ?array
|
||||
{
|
||||
$request = \IPS\Http\Url::external( 'https://api.sendgrid.com/v3/' . $method )
|
||||
->request( \IPS\LONG_REQUEST_TIMEOUT )
|
||||
$request = Url::external( 'https://api.sendgrid.com/v3/' . $method )
|
||||
->request( LONG_REQUEST_TIMEOUT )
|
||||
->setHeaders( array( 'Content-Type' => 'application/json', 'Authorization' => "Bearer {$this->apiKey}" ) );
|
||||
|
||||
try
|
||||
@@ -365,44 +357,20 @@ class _SendGrid extends \IPS\Email
|
||||
|
||||
return $response;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
catch ( Exception | RuntimeException $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() );
|
||||
throw new Exception($e->getMessage(), $e->getCode() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key scopes
|
||||
*
|
||||
* @return array
|
||||
* @return array|null
|
||||
*/
|
||||
public function scopes()
|
||||
public function scopes(): ?array
|
||||
{
|
||||
return $this->_api( 'scopes' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse/Save email form settings
|
||||
*
|
||||
* @param array $values settings array
|
||||
* @return array
|
||||
*/
|
||||
public static function processSettings( array $values ): array
|
||||
{
|
||||
if ( isset( $values['mail_method'] ) and $values['mail_method'] != 'sendgrid' and Settings::i()->sendgrid_use_for == 2 )
|
||||
{
|
||||
$values['sendgrid_use_for'] = 1;
|
||||
}
|
||||
elseif ( isset( $values['mail_method'] ) and $values['mail_method'] == 'sendgrid' )
|
||||
{
|
||||
$values['sendgrid_use_for'] = 2;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
}
|
||||
+73
-119
@@ -10,77 +10,79 @@
|
||||
|
||||
namespace IPS\Email\Outgoing;
|
||||
|
||||
use IPS\Helpers\Form\Number;
|
||||
use IPS\Helpers\Form\Password;
|
||||
use IPS\Helpers\Form\Select;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Login;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Settings;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Email;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use function defined;
|
||||
use function intval;
|
||||
use function stream_socket_enable_crypto;
|
||||
use const IPS\DEFAULT_REQUEST_TIMEOUT;
|
||||
use const IPS\VERY_LONG_REQUEST_TIMEOUT;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief SMTP Email Class
|
||||
*/
|
||||
class _SMTP extends \IPS\Email
|
||||
class Smtp extends Email
|
||||
{
|
||||
/**
|
||||
* @brief SMTP Protocol ("tls", "ssl" or "plain")
|
||||
*/
|
||||
protected $smtpProtocol;
|
||||
protected string $smtpProtocol;
|
||||
|
||||
/**
|
||||
* @brief SMTP Host
|
||||
*/
|
||||
protected $smtpHost;
|
||||
protected string $smtpHost;
|
||||
|
||||
/**
|
||||
* @brief SMTP Port
|
||||
*/
|
||||
protected $smtpPort;
|
||||
protected int $smtpPort;
|
||||
|
||||
/**
|
||||
* @brief SMTP Username
|
||||
*/
|
||||
protected $smtpUser;
|
||||
protected string $smtpUser;
|
||||
|
||||
/**
|
||||
* @brief SMTP Password
|
||||
*/
|
||||
protected $smtpPass;
|
||||
protected string $smtpPass;
|
||||
|
||||
/**
|
||||
* @brief SMTP Connections
|
||||
*/
|
||||
protected static $smtp = array();
|
||||
protected static array $smtp = array();
|
||||
|
||||
/**
|
||||
* @brief Connection Key
|
||||
*/
|
||||
protected $connectionKey;
|
||||
protected string $connectionKey;
|
||||
|
||||
/**
|
||||
* @brief Log
|
||||
*/
|
||||
protected $log = '';
|
||||
protected string $log = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $smtpProtocol Protocol to use
|
||||
* @param string $smtpHost Hostname to connect to
|
||||
* @param int $smtpPort Port to connect to
|
||||
* @param string $smtpUser Username
|
||||
* @param string $smtpPass Password
|
||||
* @param string $smtpProtocol Protocol to use
|
||||
* @param string $smtpHost Hostname to connect to
|
||||
* @param int $smtpPort Port to connect to
|
||||
* @param string $smtpUser Username
|
||||
* @param string $smtpPass Password
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $smtpProtocol, $smtpHost, $smtpPort, $smtpUser, $smtpPass )
|
||||
public function __construct( string $smtpProtocol, string $smtpHost, int $smtpPort, string $smtpUser, string $smtpPass )
|
||||
{
|
||||
$this->smtpProtocol = $smtpProtocol;
|
||||
$this->smtpHost = $smtpHost;
|
||||
@@ -93,10 +95,10 @@ class _SMTP extends \IPS\Email
|
||||
/**
|
||||
* Connect to server
|
||||
*
|
||||
* @param bool $checkSsl If set to FALSE, will skip peer certificate verification for TLS connections
|
||||
* @param bool $checkSsl If set to FALSE, will skip peer certificate verification for TLS connections
|
||||
* @return void
|
||||
*/
|
||||
public function connect( $checkSsl=TRUE )
|
||||
public function connect( bool $checkSsl=TRUE ) : void
|
||||
{
|
||||
/* Do we already have a connection? */
|
||||
if( array_key_exists( $this->connectionKey, static::$smtp ) )
|
||||
@@ -105,10 +107,10 @@ class _SMTP extends \IPS\Email
|
||||
}
|
||||
|
||||
/* Connect */
|
||||
$connection = @fsockopen( ( ( $this->smtpProtocol == 'ssl' ) ? 'ssl://' : '' ) . $this->smtpHost, $this->smtpPort, $errno, $errstr, \IPS\Request::isCliEnvironment() ? \IPS\VERY_LONG_REQUEST_TIMEOUT : \IPS\DEFAULT_REQUEST_TIMEOUT );
|
||||
$connection = @fsockopen( ( ( $this->smtpProtocol == 'ssl' ) ? 'ssl://' : '' ) . $this->smtpHost, $this->smtpPort, $errno, $errstr, Request::isCliEnvironment() ? VERY_LONG_REQUEST_TIMEOUT : DEFAULT_REQUEST_TIMEOUT );
|
||||
if ( !$connection )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( $errstr, $errno );
|
||||
throw new Exception( $errstr, $errno );
|
||||
}
|
||||
static::$smtp[ $this->connectionKey ] = $connection;
|
||||
register_shutdown_function(function( $object ){
|
||||
@@ -122,20 +124,19 @@ class _SMTP extends \IPS\Email
|
||||
$responseCode = mb_substr( $announce, 0, 3 );
|
||||
if ( $responseCode != 220 )
|
||||
{
|
||||
throw new \IPS\Email\Outgoing\Exception( 'smtpmail_fsock_error_initial', 0, NULL, array( $responseCode ) );
|
||||
throw new Exception( 'smtpmail_fsock_error_initial', 0, NULL, array( $responseCode ) );
|
||||
}
|
||||
|
||||
/* HELO/EHLO */
|
||||
$ehloFqdn = \IPS\Http\Url::internal('')->data['host'];
|
||||
try
|
||||
{
|
||||
$helo = 'EHLO';
|
||||
$responseCode = $this->_sendCommand( 'EHLO ' . $ehloFqdn, 250 );
|
||||
$responseCode = $this->_sendCommand('EHLO ' . $this->smtpHost, 250);
|
||||
}
|
||||
catch ( \IPS\Email\Outgoing\Exception $e )
|
||||
catch (Exception $e )
|
||||
{
|
||||
$helo = 'HELO';
|
||||
$responseCode = $this->_sendCommand( 'HELO ' . $ehloFqdn, 250 );
|
||||
$responseCode = $this->_sendCommand('HELO ' . $this->smtpHost, 250);
|
||||
}
|
||||
|
||||
/* Is TLS being used? */
|
||||
@@ -146,8 +147,8 @@ class _SMTP extends \IPS\Email
|
||||
@stream_context_set_option( static::$smtp[ $this->connectionKey ], 'ssl', 'verify_peer', false );
|
||||
}
|
||||
|
||||
$this->_sendCommand( 'STARTTLS', 220 );
|
||||
if ( !@\stream_socket_enable_crypto( static::$smtp[ $this->connectionKey ], TRUE, STREAM_CRYPTO_METHOD_SSLv23_CLIENT ) )
|
||||
$this->_sendCommand('STARTTLS', 220);
|
||||
if ( !@stream_socket_enable_crypto( static::$smtp[ $this->connectionKey ], TRUE, STREAM_CRYPTO_METHOD_SSLv23_CLIENT ) )
|
||||
{
|
||||
if ( $checkSsl )
|
||||
{
|
||||
@@ -157,20 +158,20 @@ class _SMTP extends \IPS\Email
|
||||
else
|
||||
{
|
||||
/* If it still failed on the second connection attempt, throw the exception */
|
||||
throw new \IPS\Email\Outgoing\Exception( 'smtpmail_tls_failed' );
|
||||
throw new Exception( 'smtpmail_tls_failed' );
|
||||
}
|
||||
}
|
||||
|
||||
/* Exchange server (at least) wants EHLO resending for STARTTLS */
|
||||
$this->_sendCommand( $helo . ' ' . $ehloFqdn, 250 );
|
||||
$this->_sendCommand($helo . ' ' . $this->smtpHost, 250);
|
||||
}
|
||||
|
||||
/* Authenticate */
|
||||
if ( $this->smtpUser )
|
||||
{
|
||||
$responseCode = $this->_sendCommand( 'AUTH LOGIN', 334 );
|
||||
$responseCode = $this->_sendCommand( base64_encode( $this->smtpUser ), 334 );
|
||||
$responseCode = $this->_sendCommand( base64_encode( $this->smtpPass ), 235 );
|
||||
$responseCode = $this->_sendCommand('AUTH LOGIN', 334);
|
||||
$responseCode = $this->_sendCommand(base64_encode($this->smtpUser), 334);
|
||||
$responseCode = $this->_sendCommand(base64_encode($this->smtpPass), 235);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,74 +179,69 @@ class _SMTP extends \IPS\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 Additional headers to send
|
||||
* @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
|
||||
* @throws Exception
|
||||
*/
|
||||
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
|
||||
public function _send( mixed $to, mixed $cc=array(), mixed $bcc=array(), mixed $fromEmail = NULL, mixed $fromName = NULL, array $additionalHeaders = array() ) : void
|
||||
{
|
||||
/* Get the from email */
|
||||
$fromEmail = $fromEmail ?: 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 */
|
||||
$recipientsForSmtp = explode( ',', static::_parseRecipients( $to, TRUE ) );
|
||||
$recipieintsForSmtp = explode( ',', static::_parseRecipients( $to, TRUE ) );
|
||||
if ( $cc )
|
||||
{
|
||||
$recipientsForSmtp = array_merge( $recipientsForSmtp, explode( ',', static::_parseRecipients( $cc, TRUE ) ) );
|
||||
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $cc, TRUE ) ) );
|
||||
}
|
||||
if ( $bcc )
|
||||
{
|
||||
$recipientsForSmtp = array_merge( $recipientsForSmtp, explode( ',', static::_parseRecipients( $bcc, TRUE ) ) );
|
||||
}
|
||||
$recipientsForSmtp = array_unique( array_map( 'trim', $recipientsForSmtp ) );
|
||||
|
||||
if( empty( $additonalHeaders['Message-ID'] ) )
|
||||
{
|
||||
$additionalHeaders['Message-ID'] = $this->generateMessageId();
|
||||
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $bcc, TRUE ) ) );
|
||||
}
|
||||
$recipieintsForSmtp = array_unique( array_map( 'trim', $recipieintsForSmtp ) );
|
||||
|
||||
/* Send */
|
||||
$this->_sendCompiled( $fromEmail, $recipientsForSmtp, $this->compileFullEmail( $to, $cc, array(), $fromEmail, $fromName, $additionalHeaders ) );
|
||||
$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 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
|
||||
* @return void
|
||||
* @throws \IPS\Email\Outgoing\Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function _sendCompiled( $fromEmail, $toEmails, $email )
|
||||
public function _sendCompiled( string $fromEmail, array $toEmails, string $email ) : void
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
$this->_sendCommand( "MAIL FROM:<{$fromEmail}>", 250 );
|
||||
$this->_sendCommand("MAIL FROM:<{$fromEmail}>", 250);
|
||||
|
||||
foreach ( $toEmails as $toEmail )
|
||||
{
|
||||
$this->_sendCommand( "RCPT TO:<{$toEmail}>", 250, TRUE );
|
||||
$this->_sendCommand("RCPT TO:<{$toEmail}>", 250 );
|
||||
}
|
||||
|
||||
$this->_sendCommand( 'DATA', 354 );
|
||||
$this->_sendCommand( $email . "\r\n.", 250 );
|
||||
$this->_sendCommand('DATA', 354);
|
||||
$this->_sendCommand($email . "\r\n.", 250);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMTP Command
|
||||
*
|
||||
* @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
|
||||
* @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
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function _sendCommand( $command, $expectedResponse=NULL, $resetOnFailure=FALSE )
|
||||
protected function _sendCommand( string $command, ?int $expectedResponse=NULL, bool $resetOnFailure = TRUE): string
|
||||
{
|
||||
/* Log */
|
||||
$this->log .= "> {$command}\r\n";
|
||||
@@ -257,15 +253,15 @@ class _SMTP extends \IPS\Email
|
||||
$response = $this->_getResponse();
|
||||
|
||||
/* Get response code */
|
||||
$code = \intval( mb_substr( $response, 0, 3 ) );
|
||||
$code = intval( mb_substr( $response, 0, 3 ) );
|
||||
if ( $expectedResponse !== NULL and $code !== $expectedResponse )
|
||||
{
|
||||
if( $resetOnFailure === TRUE )
|
||||
{
|
||||
$this->_sendCommand( 'RSET' );
|
||||
$this->_sendCommand('RSET');
|
||||
}
|
||||
|
||||
throw new \IPS\Email\Outgoing\Exception( $response, $code );
|
||||
throw new Exception( $response, $code );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
@@ -277,7 +273,7 @@ class _SMTP extends \IPS\Email
|
||||
*
|
||||
* @return string Response
|
||||
*/
|
||||
protected function _getResponse()
|
||||
protected function _getResponse(): string
|
||||
{
|
||||
/* Read */
|
||||
$response = '';
|
||||
@@ -296,56 +292,14 @@ class _SMTP extends \IPS\Email
|
||||
/* Return */
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random message-id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateMessageId(): string
|
||||
{
|
||||
$randomString = base_convert( microtime(false ) . '.' . Login::generateRandomString(), 10, 36 );
|
||||
|
||||
return '<' . $randomString . '@' . Url::internal('')->data[ Url::COMPONENT_HOST ] . '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SMTP log
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLog()
|
||||
public function getLog(): string
|
||||
{
|
||||
return $this->log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form fields for email handler
|
||||
* These will automatically toggle on when the email handler is selected to be used
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function form(): array
|
||||
{
|
||||
$form = [];
|
||||
$options = array( 'plain' => 'smtp_plaintext', 'ssl' => 'smtp_ssl', 'tls' => 'smtp_tls' );
|
||||
$form['smtp_host'] = new Text( 'smtp_host', Settings::i()->smtp_host, FALSE, [], NULL, NULL, NULL, 'smtp_host' );
|
||||
$form['smtp_protocol'] = new Select( 'smtp_protocol', Settings::i()->smtp_protocol, FALSE, [ 'options' => $options ], NULL, NULL, NULL, 'smtp_protocol' );
|
||||
$form['smtp_port'] = new Number( 'smtp_port', Settings::i()->smtp_port, FALSE, [], NULL, NULL, NULL, 'smtp_port' );
|
||||
$form['smtp_user'] = new Text( 'smtp_user', Settings::i()->smtp_user, FALSE, [], NULL, NULL, NULL, 'smtp_user' );
|
||||
$form['smtp_pass'] = new Password( 'smtp_pass', Settings::i()->smtp_pass, FALSE, [ 'enforceMaxLimit' => FALSE ], NULL, NULL, NULL, 'smtp_pass' ) ;
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this email class usable?
|
||||
*
|
||||
* @param string $type Email Type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUsable( string $type ): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user