Version 4.0.13.1

This commit is contained in:
Neo committed 2025-12-19 04:57:54 -08:00
1 parent f9e857d255
commit c6a86f0d9b
7517 files changed
+597400 -668041

No files matched your search

File diff suppressed because it is too large. Load diff
+755
View File
@@ -0,0 +1,755 @@
<?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'] = htmlentities($this->data['raw'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
$this->data['rawbody'] = htmlentities($this->data['rawbody'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
$this->data['quoted'] = htmlentities($this->data['quoted'], \IPS\HTMLENTITIES, 'UTF-8', FALSE);
$this->data['message'] = htmlentities($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;
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
/**
* @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.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social 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;
}
/**
* Debug Email Class
*/
class _Debug extends \IPS\Email
{
/**
* @brief Debug path to write the email files
*/
public $debugPath = '';
/**
* Send
*
* @return bool
* @throws \IPS\Email\Exception
*/
protected function _send()
{
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 ) )
{
throw new \IPS\Email\Outgoing\Exception( \IPS\Member::loggedIn()->language()->addToStack( 'no_write_email_debug', FALSE, array( 'sprintf' => array( rtrim( $this->debugPath, '/' ) . '/' . $filename ) ) ) );
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* @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.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 26 Aug 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;
}
/**
* Exception class for email errors
*/
class _Exception extends \RuntimeException
{
}
+49
View File
@@ -0,0 +1,49 @@
<?php
/**
* @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.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social 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;
}
/**
* PHP Email Class
*/
class _PHP extends \IPS\Email
{
/**
* Send
*
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _send()
{
try
{
if ( !mail( $this->headers['To'], mb_encode_mimeheader( $this->subject ), $this->message, $this->_compileHeaders( TRUE ), \IPS\Settings::i()->php_mail_extra ) )
{
$error = error_get_last();
preg_match( "/^(\d+)/", $error['message'], $matches );
throw new \IPS\Email\Outgoing\Exception( $error['message'], isset( $matches[1] ) ? $matches[1] : NULL );
}
}
catch ( \Exception $e )
{
throw new \IPS\Email\Outgoing\Exception( $e->getMessage() );
}
return TRUE;
}
}
+225
View File
@@ -0,0 +1,225 @@
<?php
/**
* @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.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social 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;
}
/**
* @brief SMTP Email Class
*/
class _SMTP extends \IPS\Email
{
/**
* @brief SMTP Conversation Log
*/
public $log;
/**
* Constructor
*
* @return void
*/
public function __construct()
{
$this->log = '';
}
/**
* 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
*
* @return void
*/
public function connect()
{
$tls = FALSE;
/* Check for tls:// in the hostname */
if ( mb_strpos( \IPS\Settings::i()->smtp_host, 'tls://' ) !== FALSE )
{
$tls = TRUE;
\IPS\Settings::i()->smtp_host = mb_substr( \IPS\Settings::i()->smtp_host, 6 );
}
/* Do we already have a connection? */
if( static::$smtp !== NULL )
{
return;
}
/* Connect */
static::$smtp = @fsockopen( \IPS\Settings::i()->smtp_host, \IPS\Settings::i()->smtp_port, $errno, $errstr );
if ( !static::$smtp )
{
static::$smtp = NULL;
throw new \IPS\Email\Outgoing\Exception( $errstr, $errno );
}
/* 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 ) ) ) );
}
/* If we use ssl:// we need to strip that out */
$host = \IPS\Settings::i()->smtp_host;
if( mb_strpos( \IPS\Settings::i()->smtp_host, '://' ) !== FALSE )
{
$host = mb_substr( \IPS\Settings::i()->smtp_host, mb_strpos( \IPS\Settings::i()->smtp_host, '://' ) + 3 );
}
/* HELO/EHLO */
try
{
$helo = 'EHLO';
$responseCode = $this->_sendCommand( 'EHLO ' . $host, 250 );
}
catch ( \IPS\Email\Outgoing\Exception $e )
{
$helo = 'HELO';
$responseCode = $this->_sendCommand( 'HELO ' . $host, 250 );
}
/* Is TLS being used? */
if( $tls === TRUE )
{
$this->_sendCommand( 'STARTTLS', 220 );
if ( !@\stream_socket_enable_crypto( static::$smtp, TRUE, 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 . ' ' . $host, 250 );
}
/* Authenticate */
if ( \IPS\Settings::i()->smtp_user )
{
$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 );
}
}
/**
* Send
*
* @throws \Exception
* @note We use $this->from here instead of the 'From' header, because the header may have a display name embedded
*/
protected function _send()
{
$this->connect();
$this->_sendCommand( "MAIL FROM:<{$this->from}>", 250 );
foreach ( $this->to as $to )
{
$this->_sendCommand( "RCPT TO:<{$to}>", 250 );
}
$this->_sendCommand( 'DATA', 354 );
$this->_sendCommand( "{$this->_compileHeaders()}\r\n\r\n{$this->message}\r\n.", 250 );
}
/**
* Send SMTP Command
*
* @param string $command The command
* @param int $expectedResponse The expected response code. Will throw an exception if different.
* @return string Response
* @throws \IPS\Email\Outgoing\Exception
*/
protected function _sendCommand( $command, $expectedResponse=NULL )
{
/* Send */
$this->log .= "> {$command}\r\n";
fputs( static::$smtp, $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 )
{
throw new \IPS\Email\Outgoing\Exception( $response, $code );
}
/* Return */
return $response;
}
/**
* Get response
*
* @return string Response
*/
protected function _getResponse()
{
/* Read */
$response = '';
while ( $line = @fgets( static::$smtp, 515 ) )
{
$response .= $line;
if ( mb_substr($line, 3, 1) == " " )
{
break;
}
}
return $response;
}
/**
* Return the SMTP log
*
* @return string
*/
public function getLog()
{
return $this->log;
}
}
View File
Whitespace-only changes.
+106
View File
@@ -0,0 +1,106 @@
<?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
@@ -0,0 +1,220 @@
<?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.
View File
Whitespace-only changes.