<?php
/**
* @brief Incoming email parsing and routing
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 12 June 2013
*/
namespace IPS\Email\Incoming;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* PHP Email Class
* @see <a href="https://tools.ietf.org/html/rfc5322">RFC 5322</a>
*/
class _Email
{
/**
* @brief "To" email addresses
*/
public $to = array();
/**
* @brief "CC" email addresses
*/
public $cc = array();
/**
* @brief "From" email address
*/
public $from = '';
/**
* @brief Subject
*/
public $subject = '';
/**
* @brief Message (sanatised HTML)
*/
public $message = '';
/**
* @brief Quoted part of the message (sanatised HTML)
*/
public $quote = '';
/**
* @brief Attachments
*/
public $attachments = array();
/**
* @brief 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( mt_rand() );
}
try
{
/* Save the file */
$file = \IPS\File::create( 'core_Attachment', $filename, $data['body'] );
/* Add to the message */
if ( $file->isImage() )
{
if ( isset( $data['headers']['content-id'] ) and $contentId = trim( $data['headers']['content-id'], '<>' ) and isset( $this->imagePlaceholders[ $contentId ] ) )
{
$imageTag = "<img src='<fileStore.core_Attachment>/{$file}' class='ipsImage ipsImage_thumbnailed'>";
$this->message = preg_replace( "/<a href=\"#image-placeholder={$this->imagePlaceholders[ $contentId ]}\" rel=\"[^\"]*\"><\/a>/", $imageTag, $this->message );
}
else
{
$this->message .= "<img src='<fileStore.core_Attachment>/{$file}' class='ipsImage ipsImage_thumbnailed'>";
}
}
else
{
$this->message .= "<a class='ipsAttachLink' href='<fileStore.core_Attachment>/{$file}'>{$file->originalFilename}</a>";
}
/* Save to the $attachments array and exit */
$this->attachments[] = $file;
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'incoming_email' );
/* This could be a corrupt file, which we can't do anything about */
}
}
}
}
/**
* Parse multipart parts
*
* @param array $headers Raw headers for this part
* @param array $parts Raw message sub-parts for this part
* @return void
*/
protected function _parseMultipartPart( $headers, $parts )
{
$contentType = $this->_parseStructuredHeader( $headers['content-type'] );
/* multipart/alternative is the same content in different formats (usually html and plaintext) */
if ( $contentType['value'] == 'multipart/alternative' )
{
$preferredPart = null;
foreach ( $parts as $part )
{
if ( $preferredPart === null )
{
$preferredPart = $part;
}
else
{
$preferredPartContentType = $this->_parseStructuredHeader( $preferredPart['headers']['content-type'] );
$thisPartContentType = $this->_parseStructuredHeader( $part['headers']['content-type'] );
if ( $preferredPartContentType['value'] == 'text/plain' and $thisPartContentType['value'] != 'text/plain' )
{
$preferredPart = $part;
}
}
}
$this->_parseMessagePart( $preferredPart );
}
/* Everything else (normally multipart/mixed) we'll assume to be broken into multiple parts (for example, attachments) */
else
{
foreach ( $parts as $part )
{
$this->_parseMessagePart( $part );
}
}
}
/**
* @brief Image placeholders
*/
protected $imagePlaceholders = array();
/**
* Turn body into sanatised HTML for use within Invision Community
*
* @param string $body Email body
* @param array $contentType Content-Type data (as returned by _parseStructuredHeader)
* @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 = mt_rand();
$this->imagePlaceholders[ $matches[1] ] = $id;
return '<a href="#image-placeholder=' . $id . '" rel=""></a>';
}
/**
* Extract quote from message body
*
* @param string $body Email body (the quote will be removed from it and returned)
* @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;
}
}