Version 4.1.19

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

No files matched your search

+1004 -944
View File
File diff suppressed because it is too large. Load diff
-755
View File
@@ -1,755 +0,0 @@
<?php
/**
* @brief Incoming email parsing and routing
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 12 June 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email;
/* 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 _Incoming
{
/**
* @brief Debug mode enabled
*/
protected $debugMode = FALSE;
/**
* @brief Basic email details we need to store
*/
public $data = array(
'to' => '',
'from' => '',
'subject' => '',
'cc' => '',
'message' => '',
'quoted' => '',
'raw' => '',
'rawbody' => '',
'alternative' => '',
'attachments' => array(),
);
/**
* @brief Track attachment increments so we can reference them
*/
protected $attachmentCount = 0;
/**
* @brief Store a reference to the PEAR mail object
*/
public $mail = NULL;
/**
* Initialize an instance of the class
*
* @param string $email Email text complete with headers
* @return \IPS\Email\Incoming
*/
public static function i( $email )
{
$obj = new static();
return $obj->parse( $email );
}
/**
* Enable or disable debug mode
*
* @param bool $enable Enable or disable
* @return \IPS\Email\Incoming
*/
public function setDebugMode( $enable )
{
$this->debugMode = (bool) $enable;
return $this;
}
/**
* Parse the email into pieces
*
* @param string $email Email text complete with headers
* @return \IPS\Email\Incoming
*/
protected function parse( $email )
{
$this->data['raw'] = $email;
/* Get the PEAR email library to parse the email */
require_once \IPS\ROOT_PATH . "/system/3rd_party/PEAR/Mail/mimeDecode.php";
$decoder = new \Mail_mimeDecode( $email );
$this->mail = @$decoder->decode( array(
'include_bodies' => TRUE,
'decode_bodies' => TRUE,
'decode_headers' => TRUE,
) );
/* Assign default 'to' addresses */
$to = array();
if ( mb_strpos( $this->mail->headers['to'], ',' ) === FALSE )
{
$this->mail->headers['to'] = array( $this->mail->headers['to'] );
}
else
{
$this->mail->headers['to'] = explode( ',', $this->mail->headers['to'] );
}
foreach ( $this->mail->headers['to'] as $_to )
{
if ( preg_match( "/.+? <(.+?)>/", $_to, $matches ) )
{
$to[] = $matches[1];
}
else
{
$to[] = trim( $_to, '<>' );
}
}
$this->data['to'] = implode( ',', $to );
/* Assign default 'from' address */
if ( preg_match( "/.+? <(.+?)>/", $this->mail->headers['from'], $matches ) )
{
$this->data['from'] = $matches[1];
}
else
{
$this->data['from'] = trim( $this->mail->headers['from'], '<>' );
}
/* Assign the subject */
$this->data['subject'] = ( (bool) trim( $this->mail->headers['subject'] ) ) ? $this->mail->headers['subject'] : \IPS\Member::load( $this->data['from'], 'email' )->language()->addToStack('incoming_email_no_subject');
/* Assign any 'CC' addresses */
$cc = array();
if( !empty($this->mail->headers['cc']) )
{
if( is_array( $this->mail->headers['cc'] ) )
{
$this->mail->headers['cc'] = implode( ",", $this->mail->headers['cc'] );
}
$this->mail->headers['cc'] = preg_replace( '/".+?" <(.+?)>/', '$1', $this->mail->headers['cc'] );
if ( mb_strpos( $this->mail->headers['cc'], ',' ) === FALSE )
{
$this->mail->headers['cc'] = array( $this->mail->headers['cc'] );
}
else
{
$this->mail->headers['cc'] = explode( ',', $this->mail->headers['cc'] );
}
foreach ( $this->mail->headers['cc'] as $_cc )
{
if ( preg_match( "/.+? <(.+?)>/", $_cc, $matches ) )
{
$cc[] = $matches[1];
}
else
{
$cc[] = trim( $_cc, '<> ' );
}
}
}
$this->data['cc'] = implode( ',', $cc );
/* Now start processing the body */
$this->data['message'] = '';
$this->_parsePart( $this->mail );
/* Clean up the message before passing to HTML Purifier */
$this->data['message'] = str_replace( '&nbsp;', ' ', $this->data['message'] );
/* Parse the message */
$this->data['message'] = \IPS\Text\Parser::parseStatic( $this->data['message'], FALSE, NULL, \IPS\Member::load( $this->data['from'], 'email' ), TRUE, TRUE, TRUE, function( $config ){
$config->set( 'HTML.TargetBlank', TRUE );
$config->set( 'URI.Munge', (string) \IPS\Http\Url::internal( 'app=core&module=system&controller=redirect&url=%s&key=%t&resource=%r', 'front' ) );
$config->set( 'URI.MungeResources', TRUE );
$config->set( 'URI.MungeSecretKey', md5( \IPS\Settings::i()->sql_pass . \IPS\Settings::i()->base_url . \IPS\Settings::i()->sql_database ) );
});
/* Parse the quote too if any */
if( $this->data['quoted'] )
{
$this->data['quoted'] = \IPS\Text\Parser::parseStatic( $this->data['quoted'], FALSE, NULL, \IPS\Member::load( $this->data['from'], 'email' ), TRUE, TRUE, TRUE, function( $config ){
$config->set( 'HTML.TargetBlank', TRUE );
$config->set( 'URI.Munge', (string) \IPS\Http\Url::internal( 'app=core&module=system&controller=redirect&url=%s&key=%t&resource=%r', 'front' ) );
$config->set( 'URI.MungeResources', TRUE );
$config->set( 'URI.MungeSecretKey', md5( \IPS\Settings::i()->sql_pass . \IPS\Settings::i()->base_url . \IPS\Settings::i()->sql_database ) );
});
}
/* And return */
return $this;
}
/**
* Override basic values extracted from the email.
*
* @param array $override [Optional] Pass an array that allows you to override data in the email
* @return \IPS\Email\Incoming
*/
public function setOverrides( $override )
{
/* Are we overriding any of the values? */
if( count($override) )
{
foreach( $override as $type => $value )
{
$this->data[ $type ] = $value;
}
}
return $this;
}
/**
* Route the email and pass off to the appropriate handler
*
* @return void
*/
public function route()
{
/* If debug mode is enabled, print out the appropriate data - we don't routing to happen in this case */
if( $this->debugMode )
{
print "Is this an autoresponder? ";
var_dump($this->isAutoreply());
print "<hr>";
print $this->data['message'];
print '<hr>';
print $this->data['quoted'];
print '<pre>';
$this->data['raw'] = htmlspecialchars($this->data['raw'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
$this->data['rawbody'] = htmlspecialchars($this->data['rawbody'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
$this->data['quoted'] = htmlspecialchars($this->data['quoted'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
$this->data['message'] = htmlspecialchars($this->data['message'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
print_r( $this->data );
//print_r( $this->mail );
print '</pre>';
exit;
}
/* 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':
$analyse = $this->data['to'];
break;
case 'from':
$analyse = $this->data['from'];
break;
case 'sbjt':
$analyse = $this->data['subject'];
break;
case 'body':
$analyse = $this->data['message'];
break;
}
/* Now check if we match the supplied rule */
switch ( $row['rule_criteria_type'] )
{
case 'ctns':
$match = (bool) ( mb_strpos( $analyse, $row['rule_criteria_value'] ) !== FALSE );
break;
case 'eqls':
if ( mb_strpos( $analyse, ',' ) !== FALSE )
{
$match = (bool) in_array( $analyse, explode( ',', $analyse ) );
}
else
{
$match = (bool) ( $analyse == $row['rule_criteria_value'] );
}
break;
case 'regx':
$match = (bool) ( preg_match( "/{$row['rule_criteria_value']}/", $analyse ) == 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' ) )
{
$email = \IPS\Email::buildFromTemplate( 'core', 'unrouted', $this->data );
$email->from = $this->data['to'];
$email->subject = 'Re: ' . $this->data['subject'];
$email->send( $this->data['from'] );
}
}
}
/**
* Is this an auto-reply? Try to detect to prevent auto-reply loops.
*
* @return bool
* @link https://github.com/opennorth/multi_mail/wiki/Detecting-autoresponders
*/
protected function isAutoreply()
{
/* RFC http://tools.ietf.org/html/rfc3834 */
if( !empty($this->mail->headers['auto-submitted']) AND mb_strtolower($this->mail->headers['auto-submitted']) != 'no' )
{
return TRUE;
}
/* If any of these headers are present with any values, ignore the email */
if( !empty($this->mail->headers['x-auto-response-suppress']) OR
!empty($this->mail->headers['x-autorespond']) OR
!empty($this->mail->headers['x-autoreply']) OR
!empty($this->mail->headers['x-autoreply-From']) OR
!empty($this->mail->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->mail->headers['return-path']) AND $this->mail->headers['return-path'] == '<>' )
{
return TRUE;
}
/* Now check for specific headers with specific values */
if( !empty($this->mail->headers['x-autogenerated']) AND in_array( mb_strtolower($this->mail->headers['x-autogenerated']), array( 'forward', 'group', 'letter', 'mirror', 'redirect', 'reply' ) ) )
{
return TRUE;
}
if( !empty($this->mail->headers['precedence']) AND in_array( mb_strtolower($this->mail->headers['precedence']), array( 'auto_reply', 'list', 'bulk' ) ) )
{
return TRUE;
}
if( !empty($this->mail->headers['x-precedence']) AND in_array( mb_strtolower($this->mail->headers['x-precedence']), array( 'auto_reply', 'list', 'bulk' ) ) )
{
return TRUE;
}
if( !empty($this->mail->headers['x-fc-machinegenerated']) AND mb_strtolower($this->mail->headers['x-fc-machinegenerated']) == 'true' )
{
return TRUE;
}
if( !empty($this->mail->headers['x-post-messageclass']) AND mb_strtolower($this->mail->headers['x-post-messageclass']) == '9; autoresponder' )
{
return TRUE;
}
if ( !empty( $this->mail->headers['delivered-to'] ) AND is_array( $this->mail->headers['delivered-to'] ) )
{
foreach( $this->mail->headers['delivered-to'] AS $deliveredTo )
{
if ( mb_strtolower( $deliveredTo ) == 'autoresponder' )
{
return TRUE;
}
}
}
if( !empty($this->mail->headers['delivered-to']) AND is_string( $this->mail->headers['delivered-to'] ) AND mb_strtolower($this->mail->headers['delivered-to']) == 'autoresponder' )
{
return TRUE;
}
/* And finally, some basic checks on the subject line */
if( mb_stripos( $this->mail->headers['subject'], "out of office: " ) === 0 OR
mb_stripos( $this->mail->headers['subject'], "out of office autoreply:" ) === 0 OR
mb_stripos( $this->mail->headers['subject'], "automatic reply: " ) === 0 OR
mb_strtolower($this->mail->headers['subject']) === "out of office"
)
{
return TRUE;
}
return FALSE;
}
/**
* Parse a "part"
*
* @param stdClass As returned by mailMime_decode
*/
protected function _parsePart( $part )
{
/* Some clients use uppercase, but we check by lowercase */
$part->ctype_primary = mb_strtolower( $part->ctype_primary );
$part->ctype_secondary = mb_strtolower( $part->ctype_secondary );
/* Make sure .txt files get attached as .txt files */
if ( $part->ctype_primary == 'text' AND isset($part->disposition) AND $part->disposition == 'attachment' )
{
$part->ctype_primary = 'unknown';
$part->ctype_secondary = 'unknown';
}
/* What is this? */
switch ( $part->ctype_primary )
{
/* Multipart - means there's more than one part to this part so we have to figure out which to use */
case 'multipart':
/* "Alternative" means there's more than one way to interpret this part, usually means we have plain text and HTML varients */
if ( $part->ctype_secondary == 'alternative' )
{
/* First, check if we have html and plaintext */
if ( !$this->data['alternative'] )
{
$haveHtml = false;
$havePlain = false;
foreach ( $part->parts as $p )
{
if ( $p->ctype_primary == 'text' )
{
if ( $p->ctype_secondary == 'html' )
{
$haveHtml = true;
}
elseif ( $p->ctype_secondary == 'plain' )
{
$havePlain = true;
}
}
}
}
/* Determine which part is better for us */
$preferredPart = array_shift( $part->parts );
foreach ( $part->parts as $p )
{
if ( $this->_isBetter( $preferredPart, $p ) )
{
$preferredPart = $p;
}
}
/* What did we choose? */
if ( !$this->data['alternative'] and $haveHtml and $havePlain )
{
if ( $preferredPart->ctype_secondary == 'html' )
{
$this->data['alternative'] = 'h';
}
elseif ( $preferredPart->ctype_secondary == 'html' )
{
$this->data['alternative'] = 'p';
}
}
return $this->_parsePart( $preferredPart );
}
/* Otherwise, parse all parts */
foreach ( $part->parts as $p )
{
$this->_parsePart( $p );
}
return;
/* Parse text (email body) */
case 'text':
$body = $part->body;
$this->data['rawbody'] .= $body;
/* If this is a plaintext email, we need to convert newlines to line breaks */
if ( $part->ctype_secondary != 'html' )
{
$body = nl2br( $body );
}
/* Convert the character set if necessary */
if ( isset( $part->ctype_parameters['charset'] ) and mb_strtolower($part->ctype_parameters['charset']) != 'utf-8' )
{
if( in_array( mb_strtolower($part->ctype_parameters['charset']), array_map( 'mb_strtolower', mb_list_encodings() ) ) )
{
$body = mb_convert_encoding( $body, 'UTF-8', $part->ctype_parameters['charset'] );
$part->ctype_parameters['charset'] = 'UTF-8';
}
}
/* Parse out > style quotes - mainly plaintext emails */
$_quote = array();
$_body = array();
$_seen = FALSE;
$inQuote = FALSE;
$exploded = explode( "<br />", $body );
if ( count( $exploded ) === 1 )
{
$exploded = explode( '<br>', $body );
}
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( $_quote, "<br />" );
$message = implode( $_body, "<br />" );
/* 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 */
$_lastClosingBlockquote = strrpos( $message, "</blockquote>" );
/* 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( '/<div([^>]+?)?>((?!<div).)*:(<br(>| \/>))?\s*<\/div>/s', $message, $header, PREG_OFFSET_CAPTURE );
if( !empty($header) AND $header[0][1] < $matches[0][1] )
{
$matches[0][1] = $header[0][1];
}
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. The "13" here is "</blockquote>". */
$quote = mb_substr( $message, $matches[0][1], ( $_lastClosingBlockquote - $matches[0][1] ) + 13 );
/* And finally, remove the quoted stuff from our email message */
$message = substr_replace( $message, '', $matches[0][1], ( $_lastClosingBlockquote - $matches[0][1] ) + 13 );
}
}
$this->data['quoted'] .= $quote;
$this->data['message'] .= $message;
return;
/* Parse attachments */
default:
if( !$part->body )
{
return;
}
if ( $part->ctype_primary == '_text' )
{
$part->ctype_primary = 'text';
}
$this->attachmentCount++;
/* Get the details for this file */
$mime = "{$part->ctype_primary}/{$part->ctype_secondary}";
$name = ( !empty($part->ctype_parameters['name']) ) ? $part->ctype_parameters['name'] : $part->d_parameters['filename'];
$ext = mb_substr( $name, ( mb_strrpos( $name, '.' ) + 1 ) );
/* Store the attachment. Later, classes can just call this object ->makeAttachment() passing an appropriate post-key in. */
$file = \IPS\File::create( 'core_Attachment', $name, $part->body );
$this->data['attachments'][ $this->attachmentCount ] = $file;
if ( $file->isImage() )
{
$this->data['message'] .= "<img src='{$file}' class='ipsImage ipsImage_thumbnailed'>";
}
else
{
$this->data['message'] .= "<a class='ipsAttachLink' href='{$file}'>{$file->originalFilename}</a>";
}
return;
}
}
/**
* Decide if one part is better than another for parsing multipart/alternative
*
* @param stdClass $part1 Part 1
* @param stdClass $part2 Part 2
* @return bool
*/
protected function _isBetter( $part1, $part2 )
{
/* Define our types */
$p1Primary = $part1->ctype_primary;
$p1Secondary = $part1->ctype_secondary;
$p2Primary = $part2->ctype_primary;
$p2Secondary = $part2->ctype_secondary;
/* If they're the same, return false */
if ( $p1Primary == $p2Primary and $p1Secondary == $p2Secondary )
{
return false;
}
/* Do we have a multipart email? Use that first, as it will contain the attachments */
if( $p1Primary == 'multipart' )
{
if( $p2Primary != 'multipart' )
{
return false;
}
}
else if( $p2Primary == 'multipart' )
{
return true;
}
/* Check for a text email - check for html first and then fall back to plaintext */
if( $p1Primary == 'text' )
{
if( $p2Primary == 'text' )
{
if( $p1Secondary == 'html' )
{
return false;
}
else if( $p2Secondary == 'html' )
{
return true;
}
else if( $p1Secondary == 'plain' )
{
return false;
}
else if( $p2Secondary == 'plain ')
{
return true;
}
}
}
else if( $p2Primary == 'text' )
{
return true;
}
/* If we're still here, neither part satisfies our requests - assume Part 1 is better */
return false;
}
}
+845
View File
@@ -0,0 +1,845 @@
<?php
/**
* @brief Incoming email parsing and routing
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 12 June 2013
* @version SVN_VERSION_NUMBER
*/
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 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' )
{
$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
* @retrun void
*/
protected function _parseBasicHeaders( $headers )
{
/* To */
if ( $headers['to'] )
{
if ( mb_strpos( $headers['to'], ',' ) === FALSE )
{
$this->to = array( $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'];
}
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( uniqid() );
}
/* 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>";
}
/* Save to the $attachments array and exit */
$this->attachments[] = $file;
}
}
}
/**
* 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 IPS Community Suite
*
* @param string $body Email body
* @param array $contentType Content-Type data (as returned by _parseStructuredHeader)
* @retrun 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 */
$body = \IPS\Text\Parser::parseStatic( $body, FALSE, NULL, \IPS\Member::load( $this->from, 'email' ), TRUE, TRUE, TRUE, function( $config ) {
$config->set( 'HTML.TargetBlank', TRUE );
$config->set( 'URI.Munge', urldecode( (string) \IPS\Http\Url::internal( 'app=core&module=system&controller=redirect&url=%s&key=%t&resource=%r', 'front' ) ) );
$config->set( 'URI.MungeResources', TRUE );
$config->set( 'URI.MungeSecretKey', \IPS\Settings::i()->site_secret_key );
});
/* 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 = md5( uniqid() );
$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)
* @retrun string
*/
protected function _extractQuoteFromBody( &$body )
{
/* Init */
$_quote = array();
$_body = array();
$_seen = FALSE;
$inQuote = FALSE;
$exploded = explode( "<br />", $body );
if ( count( $exploded ) === 1 )
{
$exploded = explode( '<br>', $body );
}
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___>', '{___base_url___}', $body );
$quote = str_replace( '<___base_url___>', '{___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':
$analyse = implode( ',', $this->to );
break;
case 'from':
$analyse = $this->from;
break;
case 'sbjt':
$analyse = $this->subject;
break;
case 'body':
$analyse = $this->message;
break;
}
/* Now check if we match the supplied rule */
switch ( $row['rule_criteria_type'] )
{
case 'ctns':
$match = (bool) ( mb_strpos( $analyse, $row['rule_criteria_value'] ) !== FALSE );
break;
case 'eqls':
if ( mb_strpos( $analyse, ',' ) !== FALSE )
{
$match = (bool) in_array( $analyse, explode( ',', $analyse ) );
}
else
{
$match = (bool) ( $analyse == $row['rule_criteria_value'] );
}
break;
case 'regx':
$match = (bool) ( preg_match( "/{$row['rule_criteria_value']}/", $analyse ) == 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 )->send( $this->from );
}
}
}
/**
* Is this an auto-reply? Try to detect to prevent auto-reply loops.
*
* @return bool
* @link 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-auto-response-suppress'] ) OR
!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;
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
/**
* @brief POP/IMAP Incoming Email Handler
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 21 December 2015
* @version SVN_VERSION_NUMBER
*/
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 $email 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 $email 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 );
}
}
@@ -0,0 +1,27 @@
<?php
/**
* @brief POP/IMAP Exception class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 21 December 2015
* @version SVN_VERSION_NUMBER
*/
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
{
}
+39 -34
View File
@@ -2,9 +2,9 @@
/**
* @brief Debug Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
@@ -26,45 +26,50 @@ class _Debug extends \IPS\Email
/**
* @brief Debug path to write the email files
*/
public $debugPath = '';
protected $debugPath = '';
/**
* Send
*
* @return bool
* @throws \IPS\Email\Exception
* @brief If sending a bulk email to more than MAX_EMAILS_PER_GO - does this
* class require waiting between cycles? For "standard" classes like
* PHP and SMTP, this will be TRUE - and will cause bulk mails to go
* to a class. For APIs like SparkPost, this can be FALSE
*/
protected function _send()
const REQUIRES_TIME_BREAK = TRUE;
/**
* Constructor
*
* @param string $debugPath Debug path
* @return void
*/
public function __construct( $debugPath )
{
$this->debugPath = $debugPath;
}
/**
* Send the email
*
* @param mixed $to The member or email address, or array of members or email addresses, to send to
* @param mixed $cc Addresses to CC (can also be email, member or array of either)
* @param mixed $bcc Addresses to BCC (can also be email, member or array of either)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
if( !is_dir( $this->debugPath ) )
{
throw new \IPS\Email\Outgoing\Exception( sprintf( \IPS\Member::loggedIn()->language()->get('no_path_email_debug'), $this->debugPath ) );
}
$debugString = $this->subject . "\n";
$debugString .= "To: " . implode( ', ', $this->to ) . "\n";
$debugString .= str_repeat( '-', 40 ) . "\n";
$debugString .= $this->_compileHeaders() . "\n";
$debugString .= str_repeat( '-', 40 ) . "\n";
$debugString .= $this->message;
$fileName = date("M-j-Y") . '-' . microtime() . '-' . urlencode( mb_substr( implode( ',', $this->to ), 0, 200 ) ) . ".eml";
$this->writeLog( $fileName, $debugString );
return TRUE;
}
/**
* Write the debug file
*
* @param string $filename File name
* @param string $data Data to write to the file
* @return void
*/
protected function writeLog( $filename, $data )
{
if( !@\file_put_contents( rtrim( $this->debugPath, '/' ) . '/' . $filename, $data ) )
$fullEmailContents = $this->compileFullEmail( $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders );
$filename = date("M-j-Y") . '-' . microtime() . '-' . urlencode( mb_substr( $this->_parseRecipients( $to, TRUE ), 0, 200 ) ) . ".eml";
if( !@\file_put_contents( rtrim( $this->debugPath, '/' ) . '/' . $filename, $fullEmailContents ) )
{
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'no_write_email_debug', FALSE, array( 'sprintf' => array( rtrim( $this->debugPath, '/' ) . '/' . $filename ) ) ) );
}
+2 -2
View File
@@ -2,9 +2,9 @@
/**
* @brief Exception class for email errors
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 26 Aug 2013
* @version SVN_VERSION_NUMBER
*/
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* @brief IPS CiC Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 13 Dec 2016
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Outgoing;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* IPS CiC Email Class
*/
class _IpsCic extends \IPS\Email
{
/* !Configuration */
/**
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = 200;
/**
* @brief If sending a bulk email to more than MAX_EMAILS_PER_GO - does this
* class require waiting between cycles? For "standard" classes like
* PHP and SMTP, this will be TRUE - and will cause bulk mails to go
* to a class. For APIs like SparkPost, this can be FALSE
*/
const REQUIRES_TIME_BREAK = FALSE;
/**
* @brief IPS Cloud Email Endpoint
*/
const ENDPOINT = 'http://ips-cic-email.invisioncic.com/sendEmail.php';
/**
* @brief Community
*/
protected $community;
/**
* Constructor
*
* @param string $apiKey API Key
* @return void
*/
public function __construct()
{
if ( \IPS\CIC and preg_match( '/^\/var\/www\/html\/(.+?)(?:\/|$)/i', \IPS\ROOT_PATH, $matches ) )
{
$this->community = $matches[1];
}
else
{
throw new \RuntimeException('Site not compatible with IPS Cloud Email');
}
}
/**
* Send the email
*
* @param mixed $to The member or email address, or array of members or email addresses, to send to
* @param mixed $cc Addresses to CC (can also be email, member or array of either)
* @param mixed $bcc Addresses to BCC (can also be email, member or array of either)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
\IPS\Http\Url::external( static::ENDPOINT )->request()->post( array(
'siteId' => $this->community,
'to' => explode( ',', static::_parseRecipients( $to, TRUE ) ),
'cc' => explode( ',', static::_parseRecipients( $cc, TRUE ) ),
'bcc' => explode( ',', static::_parseRecipients( $bcc, TRUE ) ),
'fromEmail' => $fromEmail ?: \IPS\Settings::i()->email_out,
'fromName' => $fromName ?: \IPS\Settings::i()->board_name,
'additionalHeaders' => $additionalHeaders,
'subject' => $this->compileSubject( static::_getMemberFromRecipients( $to ) ),
'html' => $this->compileContent( 'html', static::_getMemberFromRecipients( $to ) ),
'plaintext' => $this->compileContent( 'plaintext', static::_getMemberFromRecipients( $to ) ),
'precedence' => ( $this->type === static::TYPE_LIST ) ? 'list' : ( $this->type === static::TYPE_BULK ? 'bulk' : '' )
) );
}
}
+27 -10
View File
@@ -2,9 +2,9 @@
/**
* @brief PHP Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
@@ -24,15 +24,34 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
class _PHP extends \IPS\Email
{
/**
* Send
*
* Send the email
*
* @param mixed $to The member or email address, or array of members or email addresses, to send to
* @param mixed $cc Addresses to CC (can also be email, member or array of either)
* @param mixed $bcc Addresses to BCC (can also be email, member or array of either)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _send()
{
try
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
$boundary = "--==_mimepart_" . md5( uniqid() );
$subject = $this->compileSubject( static::_getMemberFromRecipients( $to ) );
$headers = array();
foreach( $this->_compileHeaders( $subject, $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders, $boundary ) as $k => $v )
{
if ( !mail( $this->headers['To'], mb_encode_mimeheader( $this->subject ), $this->message, $this->_compileHeaders( TRUE ), \IPS\Settings::i()->php_mail_extra ) )
if ( !in_array( $k, array( 'To', 'Subject' ) ) )
{
$headers[] = "{$k}: {$v}";
}
}
try
{
if ( !mail( $this->_parseRecipients( $to, TRUE ), static::encodeHeader( $subject ), $this->_compileMessage( static::_getMemberFromRecipients( $to ), $boundary, PHP_EOL ), implode( PHP_EOL, $headers ), \IPS\Settings::i()->php_mail_extra ) )
{
$error = error_get_last();
preg_match( "/^(\d+)/", $error['message'], $matches );
@@ -43,7 +62,5 @@ class _PHP extends \IPS\Email
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage() );
}
return TRUE;
}
}
+369
View File
@@ -0,0 +1,369 @@
<?php
/**
* @brief SendGrid Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 17 Oct 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Outgoing;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* SendGrid Email Class
*/
class _SendGrid extends \IPS\Email
{
/* !Configuration */
/**
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = 1000; // SendGrid has a hard 1000 recipients per request limit
/**
* @brief If sending a bulk email to more than MAX_EMAILS_PER_GO - does this
* class require waiting between cycles? For "standard" classes like
* PHP and SMTP, this will be TRUE - and will cause bulk mails to go
* to a class. For APIs like SparkPost, this can be FALSE
*/
const REQUIRES_TIME_BREAK = FALSE;
/**
* @brief API Key
*/
protected $apiKey;
/**
* Constructor
*
* @param string $apiKey API Key
* @return void
*/
public function __construct( $apiKey )
{
$this->apiKey = $apiKey;
}
/**
* Send the email
*
* @param mixed $to The member or email address, or array of members or email addresses, to send to
* @param mixed $cc Addresses to CC (can also be email, member or array of either)
* @param mixed $bcc Addresses to BCC (can also be email, member or array of either)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
/* Initiate the request */
$request = $this->_initRequest( $fromEmail, $fromName );
/* Add the subject and content */
$request['subject'] = $this->compileSubject( static::_getMemberFromRecipients( $to ) );
$request['content'] = array(
array(
'type' => 'text/plain',
'value' => $this->compileContent( 'plaintext', static::_getMemberFromRecipients( $to ) )
),
array(
'type' => 'text/html',
'value' => $this->compileContent( 'html', static::_getMemberFromRecipients( $to ) )
),
);
/* Add the recipients */
foreach ( array( 'to', 'cc', 'bcc' ) as $type )
{
if ( is_array( $$type ) )
{
foreach ( $$type as $recipient )
{
if ( $recipient instanceof \IPS\Member )
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient->email, 'name' => $recipient->name );
}
else
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient );
}
}
}
elseif ( $$type )
{
$recipient = $$type;
if ( $recipient instanceof \IPS\Member )
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient->email, 'name' => $recipient->name );
}
else
{
$request['personalizations'][0][ $type ][] = array( 'email' => $recipient );
}
}
}
/* Add additional headers */
$request = $this->_modifyRequestDataWithHeaders( $request, $additionalHeaders );
/* Send */
$response = $this->_api( 'mail/send', $request );
if ( isset( $response['errors'] ) )
{
throw new \IPS\Email\Outgoing\Exception( $response['errors'][0]['message'] );
}
}
/**
* Merge and Send
*
* @param array $recipients Array where the keys are the email addresses to send to and the values are an array of variables to replace
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS|Lang $language The language the email content should be in
* @return int Number of successful sends
*/
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), \IPS\Lang $language )
{
/* Initiate the request */
$request = $this->_initRequest( $fromEmail, $fromName );
/* Add the subject and content */
$subject = $this->compileSubject( NULL, $language );
$htmlContent = $this->compileContent( 'html', FALSE, $language );
$plaintextContent = $this->compileContent( 'plaintext', FALSE, $language );
$request['subject'] = $subject;
$request['content'] = array(
array(
'type' => 'text/plain',
'value' => $plaintextContent
),
array(
'type' => 'text/html',
'value' => $htmlContent
),
);
/* Add the recipients */
$addresses = array();
foreach ( $recipients as $email => $substitutions )
{
$addresses[] = $email;
$finalSubstitutions = array();
foreach ( $substitutions as $k => $v )
{
$language->parseEmail( $v );
$finalSubstitutions["*|{$k}|*"] = $v;
}
$request['personalizations'][] = array(
'to' => array( array( 'email' => $email ) ),
'substitutions' => $finalSubstitutions
);
}
/* Add additional headers */
$request = $this->_modifyRequestDataWithHeaders( $request, $additionalHeaders );
/* Send */
try
{
$response = $this->_api( 'mail/send', $request );
}
catch( \IPS\Email\Outgoing\Exception $e )
{
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $e->getMessage() ),
'mlog_smtp_log' => NULL
) );
return 0;
}
$errorcount = 0;
if ( isset( $response['errors'] ) )
{
$errorcount = count( $response['errors'] );
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $response['errors'] ),
'mlog_smtp_log' => NULL
) );
}
return ( count( $recipients ) - $errorcount );
}
/**
* Create a request
*
* @param array $recipients Array where the keys are the email addresses to send to and the values are an array of variables to replace
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS|Lang $language The language the email content should be in
* @return array
*/
protected function _initRequest( $fromEmail = NULL, $fromName = NULL )
{
$request = array(
'personalizations' => array(),
'from' => array(
'email' => $fromEmail ?: \IPS\Settings::i()->email_out,
'name' => $fromName ?: \IPS\Settings::i()->board_name
),
'tracking_settings' => array(
'click_tracking' => array(
'enable' => (bool) \IPS\Settings::i()->sendgrid_click_tracking,
'enable_text' => (bool) \IPS\Settings::i()->sendgrid_click_tracking,
),
'open_tracking' => array(
'enable' => (bool) \IPS\Settings::i()->sendgrid_click_tracking,
)
)
);
if ( \IPS\Settings::i()->sendgrid_ip_pool )
{
$request['ip_pool_name'] = (bool) \IPS\Settings::i()->sendgrid_ip_pool;
}
return $request;
}
/**
* Modify the request data that will be sent to the SparkPost API with header data
*
* @param array $request SparkPost API request data
* @param array $additionalHeaders Additional headers to send
* @param array $allowedTags The tags that we want to parse
* @return array
*/
protected function _modifyRequestDataWithHeaders( $request, $additionalHeaders = array(), $allowedTags = array() )
{
/* Do we have a Reply-To? */
if ( isset( $additionalHeaders['Reply-To'] ) )
{
if ( preg_match( '/(.*)\s?<(.*)>$/', $additionalHeaders['Reply-To'], $matches ) )
{
$email = $matches[2];
$request['reply_to'] = array( 'email' => $matches[2] );
if ( $matches[1] )
{
if ( preg_match( '/^\=\?UTF\-8\?B\?(.+?)\?\=$/i', trim( $matches[1] ), $_matches ) )
{
$request['reply_to']['name'] = base64_decode( $_matches[1] );
}
}
}
unset( $additionalHeaders['Reply-To'] );
}
/* Any other headers? */
unset( $additionalHeaders['x-sg-id'] );
unset( $additionalHeaders['x-sg-eid'] );
unset( $additionalHeaders['received'] );
unset( $additionalHeaders['dkim-signature'] );
unset( $additionalHeaders['Content-Type'] );
unset( $additionalHeaders['Content-Transfer-Encoding'] );
unset( $additionalHeaders['Subject'] );
unset( $additionalHeaders['From'] );
unset( $additionalHeaders['To'] );
unset( $additionalHeaders['CC'] );
unset( $additionalHeaders['BCC'] );
if ( count( $additionalHeaders ) )
{
$request['headers'] = $additionalHeaders;
}
/* Return */
return $request;
}
/**
* Make API call
*
* @param string $method Method
* @param string $apiKey API Key
* @param array $args Arguments
* @throws \IPS\Email\Outgoing\Exception Indicates an invalid JSON response or HTTP error
* @return array|null
*/
protected function _api( $method, $args=NULL )
{
$request = \IPS\Http\Url::external( 'https://api.sendgrid.com/v3/' . $method )
->request( \IPS\LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json', 'Authorization' => "Bearer {$this->apiKey}" ) );
try
{
if ( $args )
{
$response = $request->post( json_encode( $args ) );
}
else
{
$response = $request->get();
}
if ( $response->content )
{
$response = $response->decodeJson();
}
else
{
$response = null;
}
return $response;
}
catch ( \IPS\Http\Request\Exception $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
/* Capture json decoding errors */
catch ( \RuntimeException $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
}
/**
* Get API key scopes
*
* @return array
*/
public function scopes()
{
return $this->_api( 'scopes' );
}
}
+130 -60
View File
@@ -2,9 +2,9 @@
/**
* @brief SMTP Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @package IPS Community Suite
* @since 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
@@ -24,41 +24,60 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
class _SMTP extends \IPS\Email
{
/**
* @brief SMTP Conversation Log
* @brief SMTP Protocol ("tls", "ssl" or "plain")
*/
public $log;
protected $smtpProtocol;
/**
* @brief SMTP Host
*/
protected $smtpHost;
/**
* @brief SMTP Port
*/
protected $smtpPort;
/**
* @brief SMTP Username
*/
protected $smtpUser;
/**
* @brief SMTP Password
*/
protected $smtpPass;
/**
* @brief SMTP Connections
*/
protected static $smtp = array();
/**
* @brief Connection Key
*/
protected $connectionKey;
/**
* @brief Log
*/
protected $log = '';
/**
* Constructor
*
* @return void
*/
public function __construct()
public function __construct( $smtpProtocol, $smtpHost, $smtpPort, $smtpUser, $smtpPass )
{
$this->log = '';
$this->smtpProtocol = $smtpProtocol;
$this->smtpHost = $smtpHost;
$this->smtpPort = $smtpPort;
$this->smtpUser = $smtpUser;
$this->smtpPass = $smtpPass;
$this->connectionKey = md5( $smtpProtocol . $smtpHost . $smtpPort . $smtpUser . $smtpPass );
}
/**
* Destructor
*
* @return void
*/
public function __destruct()
{
if( static::$smtp !== NULL )
{
$this->_sendCommand( 'quit' );
@fclose( static::$smtp );
static::$smtp = NULL;
}
}
/**
* @brief SMTP Connection
*/
protected static $smtp = NULL;
/**
* Connect to server
*
@@ -67,25 +86,27 @@ class _SMTP extends \IPS\Email
public function connect()
{
/* Do we already have a connection? */
if( static::$smtp !== NULL )
if( array_key_exists( $this->connectionKey, static::$smtp ) )
{
return;
}
/* Connect */
static::$smtp = @fsockopen( ( ( \IPS\Settings::i()->smtp_protocol == 'ssl' ) ? 'ssl://' : '' ) . \IPS\Settings::i()->smtp_host, \IPS\Settings::i()->smtp_port, $errno, $errstr );
if ( !static::$smtp )
$connection = @fsockopen( ( ( $this->smtpProtocol == 'ssl' ) ? 'ssl://' : '' ) . $this->smtpHost, $this->smtpPort, $errno, $errstr );
if ( !$connection )
{
static::$smtp = NULL;
throw new \IPS\Email\Outgoing\Exception( $errstr, $errno );
}
static::$smtp[ $this->connectionKey ] = $connection;
register_shutdown_function(function( $object ){
$object->_sendCommand( 'quit' );
@fclose( static::$smtp[ $object->connectionKey ] );
unset( static::$smtp[ $object->connectionKey ] );
}, $this );
/* Check the initial response is okay */
$announce = $this->_getResponse();
$this->log .= $announce;
$responseCode = mb_substr( $announce, 0, 3 );
if ( $responseCode != 220 )
{
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack('smtpmail_fsock_error_initial', FALSE, array( 'sprintf' => array( $responseCode ) ) ) );
@@ -95,79 +116,124 @@ class _SMTP extends \IPS\Email
try
{
$helo = 'EHLO';
$responseCode = $this->_sendCommand( 'EHLO ' . \IPS\Settings::i()->smtp_host, 250 );
$responseCode = $this->_sendCommand( 'EHLO ' . $this->smtpHost, 250 );
}
catch ( \IPS\Email\Outgoing\Exception $e )
{
$helo = 'HELO';
$responseCode = $this->_sendCommand( 'HELO ' . \IPS\Settings::i()->smtp_host, 250 );
$responseCode = $this->_sendCommand( 'HELO ' . $this->smtpHost, 250 );
}
/* Is TLS being used? */
if( \IPS\Settings::i()->smtp_protocol == 'tls' )
if( $this->smtpProtocol == 'tls' )
{
$this->_sendCommand( 'STARTTLS', 220 );
if ( !@\stream_socket_enable_crypto( static::$smtp, TRUE, STREAM_CRYPTO_METHOD_TLS_CLIENT ) )
if ( !@\stream_socket_enable_crypto( static::$smtp[ $this->connectionKey ], TRUE, version_compare( PHP_VERSION, '5.6.7' ) >= 0 ? STREAM_CRYPTO_METHOD_SSLv23_CLIENT : STREAM_CRYPTO_METHOD_TLS_CLIENT ) )
{
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack('smtpmail_tls_failed') );
}
/* Exchange server (at least) wants EHLO resending for STARTTLS */
$this->_sendCommand( $helo . ' ' . \IPS\Settings::i()->smtp_host, 250 );
$this->_sendCommand( $helo . ' ' . $this->smtpHost, 250 );
}
/* Authenticate */
if ( \IPS\Settings::i()->smtp_user )
if ( $this->smtpUser )
{
$responseCode = $this->_sendCommand( 'AUTH LOGIN', 334 );
$responseCode = $this->_sendCommand( base64_encode( \IPS\Settings::i()->smtp_user ), 334 );
$responseCode = $this->_sendCommand( base64_encode( \IPS\Settings::i()->smtp_pass ), 235 );
$responseCode = $this->_sendCommand( base64_encode( $this->smtpUser ), 334 );
$responseCode = $this->_sendCommand( base64_encode( $this->smtpPass ), 235 );
}
}
/**
* Send
*
* @throws \Exception
* @note We use $this->from here instead of the 'From' header, because the header may have a display name embedded
* Send the email
*
* @param mixed $to The member or email address, or array of members or email addresses, to send to
* @param mixed $cc Addresses to CC (can also be email, member or array of either)
* @param mixed $bcc Addresses to BCC (can also be email, member or array of either)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _send()
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
/* Get the from email */
$fromEmail = $fromEmail ?: \IPS\Settings::i()->email_out;
/* SMTP requires you to do CC/BCC by sending a RCPT TO command for each recipient. We'll hide BCC by not actually setting that header */
$recipieintsForSmtp = explode( ',', static::_parseRecipients( $to, TRUE ) );
if ( $cc )
{
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $cc, TRUE ) ) );
}
if ( $bcc )
{
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $bcc, TRUE ) ) );
}
$recipieintsForSmtp = array_unique( array_map( 'trim', $recipieintsForSmtp ) );
/* Send */
$this->_sendCompiled( $fromEmail, $recipieintsForSmtp, $this->compileFullEmail( $to, $cc, array(), $fromEmail, $fromName, $additionalHeaders ) );
}
/**
* Send an email
*
* @param string $fromEmail The email address to send from
* @param array $toEmails Array of email addresses to send to
* @param string $email The full email (with headers, etc.) except the Bcc header
* @param array $additionalHeaders Additional headers to send
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _sendCompiled( $fromEmail, $toEmails, $email )
{
$this->connect();
$this->_sendCommand( "MAIL FROM:<{$this->from}>", 250 );
foreach ( $this->to as $to )
$this->_sendCommand( "MAIL FROM:<{$fromEmail}>", 250 );
foreach ( $toEmails as $toEmail )
{
$this->_sendCommand( "RCPT TO:<{$to}>", 250 );
$this->_sendCommand( "RCPT TO:<{$toEmail}>", 250 );
}
$this->_sendCommand( 'DATA', 354 );
$this->_sendCommand( "{$this->_compileHeaders()}\r\n\r\n{$this->message}\r\n.", 250 );
$this->_sendCommand( $email . "\r\n.", 250 );
}
/**
* Send SMTP Command
*
* @param string $command The command
* @param int $expectedResponse The expected response code. Will throw an exception if different.
* @param string $command The command
* @param int|NULL $expectedResponse The expected response code. Will throw an exception if different.
* @param bool $resetOnFailure If the command fails, issue a RSET (reset) command
* @return string Response
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _sendCommand( $command, $expectedResponse=NULL )
protected function _sendCommand( $command, $expectedResponse=NULL, $resetOnFailure=FALSE )
{
/* Send */
/* Log */
$this->log .= "> {$command}\r\n";
fputs( static::$smtp, $command . "\r\n" );
/* Send */
fputs( static::$smtp[ $this->connectionKey ], $command . "\r\n" );
/* Read */
$response = $this->_getResponse();
$this->log .= $response;
/* Get response code */
$code = intval( mb_substr( $response, 0, 3 ) );
if ( $expectedResponse !== NULL and $code !== $expectedResponse )
{
if( $resetOnFailure === TRUE )
{
$this->_sendCommand( 'RSET' );
}
throw new \IPS\Email\Outgoing\Exception( $response, $code );
}
@@ -184,7 +250,7 @@ class _SMTP extends \IPS\Email
{
/* Read */
$response = '';
while ( $line = @fgets( static::$smtp, 515 ) )
while ( $line = @fgets( static::$smtp[ $this->connectionKey ], 515 ) )
{
$response .= $line;
if ( mb_substr($line, 3, 1) == " " )
@@ -192,10 +258,14 @@ class _SMTP extends \IPS\Email
break;
}
}
/* Log */
$this->log .= $response;
/* Return */
return $response;
}
/**
* Return the SMTP log
*
+358
View File
@@ -0,0 +1,358 @@
<?php
/**
* @brief SparkPost Email Class
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community Suite
* @since 17 Apr 2013
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Outgoing;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* SparkPost Email Class
*/
class _SparkPost extends \IPS\Email
{
/* !Configuration */
/**
* @brief The number of emails that can be sent in one "go"
*/
const MAX_EMAILS_PER_GO = 2000; // SparkPost "recommends" 2000 per cycle
/**
* @brief If sending a bulk email to more than MAX_EMAILS_PER_GO - does this
* class require waiting between cycles? For "standard" classes like
* PHP and SMTP, this will be TRUE - and will cause bulk mails to go
* to a class. For APIs like SparkPost, this can be FALSE
*/
const REQUIRES_TIME_BREAK = FALSE;
/**
* @brief API Key
*/
protected $apiKey;
/**
* Constructor
*
* @param string $apiKey API Key
* @return void
*/
public function __construct( $apiKey )
{
$this->apiKey = $apiKey;
}
/**
* Send the email
*
* @param mixed $to The member or email address, or array of members or email addresses, to send to
* @param mixed $cc Addresses to CC (can also be email, member or array of either)
* @param mixed $bcc Addresses to BCC (can also be email, member or array of either)
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders The name the email should appear from. If NULL, default setting is used
* @return void
* @throws \IPS\Email\Outgoing\Exception
*/
public function _send( $to, $cc=array(), $bcc=array(), $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array() )
{
/* Compile the recipients */
$recipients = array();
$mainToAddress = NULL;
foreach ( array_map( 'trim', explode( ',', static::_parseRecipients( $to, TRUE ) ) ) as $address )
{
if ( $mainToAddress === NULL )
{
$mainToAddress = $address;
}
$recipients[] = array( 'address' => array( 'email' => $address ) );
}
if ( $cc )
{
foreach ( array_map( 'trim', explode( ',', static::_parseRecipients( $cc, TRUE ) ) ) as $address )
{
$recipients[] = array( 'address' => array( 'email' => $address, 'header_to' => $mainToAddress ) );
}
$additionalHeaders['Cc'] = static::_parseRecipients( $cc );
}
if ( $bcc )
{
foreach ( array_map( 'trim', explode( ',', static::_parseRecipients( $bcc, TRUE ) ) ) as $address )
{
$recipients[] = array( 'address' => array( 'email' => $address, 'header_to' => $mainToAddress ) );
}
}
/* Compile the API call request data */
$request = array(
'recipients' => $recipients,
'content' => array(
'html' => static::_escapeTemplateTags( $this->compileContent( 'html', static::_getMemberFromRecipients( $to ) ) ),
'text' => static::_escapeTemplateTags( $this->compileContent( 'plaintext', static::_getMemberFromRecipients( $to ) ) ),
'subject' => static::_escapeTemplateTags( $this->compileSubject( static::_getMemberFromRecipients( $to ) ) ),
'from' => array(
'email' => $fromEmail ?: \IPS\Settings::i()->email_out,
'name' => $fromName ?: \IPS\Settings::i()->board_name
)
),
'options' => array(
'transactional' => $this->type === static::TYPE_TRANSACTIONAL,
'open_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking,
'click_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking
)
);
if ( \IPS\Settings::i()->sparkpost_ip_pool )
{
$request['options']['ip_pool'] = \IPS\Settings::i()->sparkpost_ip_pool;
}
$request = $this->_modifyRequestDataWithHeaders( $request, $additionalHeaders );
/* Make API call */
$response = $this->_api( 'transmissions', $request );
if ( isset( $response['errors'] ) )
{
throw new \IPS\Email\Outgoing\Exception( $response['errors'][0]['message'], ( isset( $response['errors'][0]['code'] ) ) ? $response['errors'][0]['code'] : NULL );
}
}
/**
* Modify the request data that will be sent to the SparkPost API with header data
*
* @param array $request SparkPost API request data
* @param array $additionalHeaders Additional headers to send
* @param array $allowedTags The tags that we want to parse
* @return array
*/
protected function _modifyRequestDataWithHeaders( $request, $additionalHeaders = array(), $allowedTags = array() )
{
/* Do we have a Reply-To? */
if ( isset( $additionalHeaders['Reply-To'] ) )
{
$request['content']['reply_to'] = static::_escapeTemplateTags( $additionalHeaders['Reply-To'], $allowedTags );
unset( $additionalHeaders['Reply-To'] );
}
/* Any other headers? */
unset( $additionalHeaders['Subject'] );
unset( $additionalHeaders['From'] );
unset( $additionalHeaders['To'] );
if ( count( $additionalHeaders ) )
{
$request['content']['headers'] = array_map( function( $v ) use ( $allowedTags ) {
return static::_escapeTemplateTags( $v, $allowedTags );
}, $additionalHeaders );
}
/* Return */
return $request;
}
/**
* Merge and Send
*
* @param array $recipients Array where the keys are the email addresses to send to and the values are an array of variables to replace
* @param mixed $fromEmail The email address to send from. If NULL, default setting is used. NOTE: This should always be a site-controlled domin. Some services like Sparkpost require the domain to be validated.
* @param mixed $fromName The name the email should appear from. If NULL, default setting is used
* @param array $additionalHeaders Additional headers to send. Merge tags can be used like in content.
* @param \IPS|Lang $language The language the email content should be in
* @return int Number of successful sends
*/
public function mergeAndSend( $recipients, $fromEmail = NULL, $fromName = NULL, $additionalHeaders = array(), \IPS\Lang $language )
{
/* Work out recipients */
$varNames = array();
$recipientsForSparkpost = array();
$addresses = array();
foreach ( $recipients as $address => $_vars )
{
$addresses[] = $address;
$vars = array();
foreach ( $_vars as $k => $v )
{
$language->parseEmail( $v );
$vars[ $k ] = $v;
if ( !in_array( $k, $varNames ) )
{
$varNames[] = $k;
}
}
$recipientsForSparkpost[] = array( 'address' => array( 'email' => $address ), 'substitution_data' => $vars );
}
/* Put tags into SparkPost format */
$htmlContent = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $this->compileContent( 'html', FALSE, $language ) );
$plaintextContent = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $this->compileContent( 'plaintext', FALSE, $language ) );
$subject = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $this->compileSubject( NULL, $language ) );
$_additionalHeaders = array();
foreach ( $additionalHeaders as $k => $v )
{
$_additionalHeaders[ $k ] = str_replace( array( '*|', '|*' ), array( '{{{', '}}}' ), $v );
}
/* Compile the API call request data */
$request = array(
'recipients' => $recipientsForSparkpost,
'content' => array(
'html' => static::_escapeTemplateTags( $htmlContent, $varNames ),
'text' => static::_escapeTemplateTags( $plaintextContent, $varNames ),
'subject' => static::_escapeTemplateTags( $subject, $varNames ),
'from' => array(
'email' => $fromEmail ?: \IPS\Settings::i()->email_out,
'name' => $fromName ?: \IPS\Settings::i()->board_name
)
),
'options' => array(
'transactional' => $this->type === static::TYPE_TRANSACTIONAL,
'open_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking,
'click_tracking' => (bool) \IPS\Settings::i()->sparkpost_click_tracking
)
);
if ( \IPS\Settings::i()->sparkpost_ip_pool )
{
$request['options']['ip_pool'] = \IPS\Settings::i()->sparkpost_ip_pool;
}
$request = $this->_modifyRequestDataWithHeaders( $request, $_additionalHeaders, $varNames );
/* Make API call */
try
{
$response = $this->_api( 'transmissions', $request );
}
catch( \IPS\Email\Outgoing\Exception $e )
{
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $e->getMessage() ),
'mlog_smtp_log' => NULL
) );
return 0;
}
if ( isset( $response['errors'] ) )
{
\IPS\Db::i()->insert( 'core_mail_error_logs', array(
'mlog_date' => time(),
'mlog_to' => json_encode( $addresses ),
'mlog_from' => $fromEmail ?: \IPS\Settings::i()->email_out,
'mlog_subject' => $subject,
'mlog_content' => $htmlContent,
'mlog_resend_data' => NULL,
'mlog_msg' => json_encode( $response['errors'] ),
'mlog_smtp_log' => NULL
) );
}
return ( isset( $response['results'] ) and isset( $response['results']['total_accepted_recipients'] ) ) ? $response['results']['total_accepted_recipients'] : 0;
}
/**
* Make API call
*
* @param string $method Method
* @param string $apiKey API Key
* @param array $args Arguments
* @throws \IPS\Email\Outgoing\Exception Indicates an invalid JSON response or HTTP error
* @return array
*/
protected function _api( $method, $args=NULL )
{
$request = \IPS\Http\Url::external( 'https://api.sparkpost.com/api/v1/' . $method )
->request( \IPS\LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json', 'Authorization' => $this->apiKey ) );
try
{
if ( $args )
{
$response = $request->post( json_encode( $args ) );
}
else
{
$response = $request->get();
}
return $response->decodeJson();
}
catch ( \IPS\Http\Request\Exception $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
/* Capture json decoding errors */
catch ( \RuntimeException $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage(), $e->getCode() );
}
}
/**
* Escape template tags
*
* @param string $content The content
* @param array $allowedTags The tags that we want to parse
* @return array
* @see <a href="https://developers.sparkpost.com/api/#/introduction/substitutions-reference/escaping-start-and-end-tags">Escaping Start and End Tags</a>
*/
protected static function _escapeTemplateTags( $content , $allowedTags = array() )
{
/* Store the tags we're allowed */
$replacedTagsI = 0;
$replacedTagsStore = array();
$content = preg_replace_callback( '/\{\{\{(' . implode( '|', array_map( function( $val ) { return preg_quote( $val, '/' ); }, $allowedTags ) ) . ')\}\}\}/', function( $matches ) use ( &$replacedTagsI, &$replacedTagsStore )
{
$replacedTagsStore[ ++$replacedTagsI ] = $matches[0];
return '--rt-' . $replacedTagsI . '--';
}, $content );
/* Escape */
$content = str_replace( '{{{', '--ot--x--', $content );
$content = str_replace( '}}}', '--ct--x--', $content );
$content = str_replace( '{{', '--od--x--', $content );
$content = str_replace( '}}', '--cd--x--', $content );
$content = str_replace( '--ot--x--', '{{opening_triple_curly()}}', $content );
$content = str_replace( '--ct--x--', '{{closing_triple_curly()}}', $content );
$content = str_replace( '--od--x--', '{{opening_double_curly()}}', $content );
$content = str_replace( '--cd--x--', '{{closing_double_curly()}}', $content );
/* Put our {{{tags}}} back */
$content = preg_replace_callback( '/--rt-(.+?)--/', function( $matches ) use ( $replacedTagsStore )
{
return isset( $replacedTagsStore[ $matches[1] ] ) ? $replacedTagsStore[ $matches[1] ] : '';
}, $content );
/* Return */
return $content;
}
/**
* Get sending domains
*
* @return array
*/
public function sendingDomains()
{
return $this->_api( 'sending-domains' );
}
}
-106
View File
@@ -1,106 +0,0 @@
<?php
/**
* @brief Magic Template Class for IN_DEV mode
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 12 May 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Theme;
/* 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;
}
/**
* Magic Template Class for IN_DEV mode
*/
class _Template extends \IPS\Theme\Template
{
/**
* @brief Source Folder
*/
public $sourceFolder = NULL;
/**
* Contructor
*
* @param string $app Application Key
* @param string $templateLocation Template location (admin/public/etc.)
* @param string $templateName Template Name
* @return void
*/
public function __construct( $app, $templateLocation, $templateName )
{
parent::__construct( $app, $templateLocation, $templateName );
$this->app = $app;
$this->templateLocation = $templateLocation;
$this->templateName = $templateName;
if ( \IPS\IN_DEV )
{
$this->sourceFolder = \IPS\ROOT_PATH . "/applications/{$app}/dev/email/{$templateLocation}/{$templateName}/";
}
}
/**
* Magic Method: Call Template Bit
*
* @param string $bit Template Bit Name
* @param array $params Parameters
* @return string
*/
public function __call( $bit, $params )
{
if ( \IPS\IN_DEV )
{
/* What are we calling this? */
$functionName = "theme_{$this->app}_{$this->templateLocation}_{$this->templateName}_{$bit}";
/* If it doesn't exist, build it */
if( !function_exists( 'IPS\\Theme\\'.$functionName ) )
{
/* Find the file */
$file = $this->sourceFolder . $bit . ( ( $this->templateLocation == 'html' ) ? '.phtml' : '.txt' );
/* Get the content */
if ( $file === NULL or !file_exists( $file ) )
{
throw new \BadMethodCallException( 'NO_TEMPLATE_FILE - ' . $file );
}
$output = file_get_contents( $file );
/* Parse the header tag */
if ( !preg_match( '/^<ips:template parameters="(.+?)?"([^>]+?)>(\r\n?|\n)/', $output, $matches ) )
{
throw new \BadMethodCallException( 'NO_HEADER - ' . $file );
}
/* Strip it */
$output = preg_replace( '/^<ips:template parameters="(.+?)?"([^>]+?)>(\r\n?|\n)/', '', $output );
/* Make it into a lovely function */
\IPS\Theme::makeProcessFunction( $output, $functionName, ( isset( $matches[1] ) ? $matches[1] : '' ), TRUE );
}
/* Run it */
ob_start();
$return = call_user_func_array( 'IPS\\Theme\\'.$functionName, $params );
if( $error = ob_get_clean() )
{
echo "<strong>{$functionName}</strong><br>{$error}<br><br><pre>{$output}";
exit;
}
/* Return */
return $return;
}
}
}
-220
View File
@@ -1,220 +0,0 @@
<?php
/**
* @brief IN_DEV Skin Set
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 12 May 2014
* @version SVN_VERSION_NUMBER
*/
namespace IPS\Email\Theme;
/* 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;
}
/**
* IN_DEV Skin set
*/
class _Theme extends \IPS\Theme
{
/**
* @brief Template Classes
*/
protected $templates;
/**
* @brief [SkinSets] Templates already loaded and evald via getTemplate()
*/
public static $calledTemplates = array();
/**
* Get currently logged in member's theme
*
* @return static
*/
public static function i()
{
return new self;
}
/**
* Write IN_DEV files
*
* @param boolean $force TRUE to rewrite templates, FALSE to check if exists first
* @return void
*/
public static function writeInDev( $force=FALSE )
{
$path = \IPS\ROOT_PATH . '/applications/content/dev/html';
if ( ! \IPS\IN_DEV OR ! is_writeable( $path ) )
{
throw new \DomainException();
}
foreach( \IPS\Db::i()->select( '*', 'content_templates', array( 'template_master=?', 1 ) ) as $template )
{
if ( ! is_dir( $path . '/' . $template['template_location'] ) )
{
mkdir( $path . '/' . $template['template_location'] );
@chmod( $path . '/' . $template['template_location'], \IPS\IPS_FOLDER_PERMISSION );
}
if ( ! is_dir( $path . '/' . $template['template_location'] . '/' . $template['template_group'] ) )
{
mkdir( $path . '/' . $template['template_location'] . '/' . $template['template_group'] );
@chmod( $path . '/' . $template['template_location'] . '/' . $template['template_group'], \IPS\IPS_FOLDER_PERMISSION );
}
$container = NULL;
if ( ! file_exists( $path . '/' . $template['template_location'] . '/' . $template['template_group'] . '/' . $template['template_key'] . '.phtml' ) OR $force === TRUE )
{
try
{
if ( $template['template_container'] )
{
$item = \IPS\content\Templates\Container::load( $template['template_container'] );
$container = array(
'name' => $item->name,
'type' => $item->type,
'parent_id' => $item->parent_id,
'key' => $item->key
);
}
}
catch( \OutofRangeException $ex ) { }
$data = json_encode( array(
'title' => $template['template_title'],
'desc' => $template['template_desc'],
'rel_id' => $template['template_rel_id'],
'container' => $container
) );
$write = '<ips:template parameters="' . $template['template_params'] . '" data="' . $data . '" />' . "\n";
$write .= $template['template_content'];
if ( ! @\file_put_contents( $path . '/' . $template['template_location'] . '/' . $template['template_group'] . '/' . $template['template_key'] . '.phtml', $write ) )
{
throw new \RuntimeException( \IPS\Member::loggedIn()->language()->addToStack( 'content_theme_dev_cannot_write_template', FALSE, array( 'sprintf' => array( $path . '/' . $template['template_location'] . '/' . $template['template_group'] . '/' . $template['template_key'] ) ) ) );
}
else
{
@chmod( $path . '/' . $template['template_location'] . '/' . $template['template_group'] . '/' . $template['template_key'] . '.phtml', 0777 );
}
}
}
}
/**
* Get a template
*
* @param string $group Template Group
* @param string $app Application key (NULL for current application)
* @param string $location Template Location (NULL for current template location)
* @return \IPS\Theme\Template
*/
public function getTemplate( $group, $app=NULL, $location=NULL )
{
/* Do we have an application? */
if( $app === NULL )
{
$app = \IPS\Dispatcher::i()->application->directory;
}
/* How about a template location? */
if( $location === NULL )
{
$location = \IPS\Dispatcher::i()->controllerLocation;
}
/* Get template */
if ( \IPS\IN_DEV )
{
if ( ! isset( $this->templates[ $app ][ $location ][ $group ] ) )
{
$this->templates[ $app ][ $location ][ $group ] = new \IPS\Email\Theme\Template( $app, $location, $group );
}
return $this->templates[ $app ][ $location ][ $group ];
}
else
{
$key = \strtolower( 'template_email_' . static::makeBuiltTemplateLookupHash( $app, $location, $group ) . '_' . $group );
/* Still here */
if ( !in_array( $key, array_keys( self::$calledTemplates ) ) )
{
/* If we don't have a compiled template, do that now */
if ( !isset( \IPS\Data\Store::i()->$key ) )
{
$functions = array( 'html' => array(), 'plain' => array() );
/* Get it from the database */
foreach( \IPS\Db::i()->select( '*', 'core_email_templates', array( "template_app=? AND template_name LIKE '" . \IPS\Db::i()->real_escape_string( $group ) . "__%'", $app ) ) as $data )
{
$funcName = mb_substr( $data['template_name'], ( mb_strlen( $group ) + 2 ) );
$functions['html'][ $funcName ] = static::compileTemplate( $data['template_content_html'], $funcName, $data['template_data'], true );
$functions['plain'][ $funcName ] = static::compileTemplate( $data['template_content_plaintext'], $funcName, $data['template_data'], true );
}
/* Put them in a class */
foreach( array( 'html', 'plain' ) as $type )
{
$var = 'template_' . $type;
${$var} = <<<EOF
namespace IPS\Theme;
class class_email_{$app}_{$type}_{$group}
{
EOF;
${$var} .= implode( "\n\n", $functions[ $type ] );
${$var} .= <<<EOF
}
EOF;
$storeKey = \strtolower( 'template_email_' . static::makeBuiltTemplateLookupHash( $app, $type, $group ) . '_' . $group );
\IPS\Data\Store::i()->$storeKey = ${$var};
}
}
/* Load compiled template */
$compiledGroup = \IPS\Data\Store::i()->$key;
if ( @eval( $compiledGroup ) === FALSE )
{
throw new \UnexpectedValueException;
}
/* Hooks */
$class = 'class_email_' . $app . '_' . $location . '_' . $group;
$class = "\IPS\Theme\\{$class}";
/* Init */
self::$calledTemplates[ $key ] = new $class();
}
return self::$calledTemplates[ $key ];
}
}
/**
* Returns the path for the IN_DEV .phtml files
* @param string $app Application Key
* @param string|null $location Location
* @param string|null $path Path or Filename
* @return string
*/
protected static function _getHtmlPath( $app, $location=null, $path=null )
{
return rtrim( \IPS\ROOT_PATH . "/applications/{$app}/dev/html/{$location}/{$path}", '/' ) . '/';
}
}
View File
Whitespace-only changes.