<?php
/**
* @brief Nexus Incoming Email Handler
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage Nexus
* @since 07 Mar 2014
*/
namespace IPS\nexus\extensions\core\IncomingEmail;
/* 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;
}
/**
* Community Enhancement
*/
class _Support
{
/**
* Handle email
*
* @param \IPS\Email\Incoming\Email $email The email
* @return bool
*/
public function process( \IPS\Email\Incoming\Email $email )
{
/* Is it from an actual member? */
$member = \IPS\Member::load( $email->from, 'email' );
/* Replying? */
$request = $this->_emailIsReplyForRequest( $email );
$department = $this->_getDepartment( $email );
if ( $request )
{
/* IS this request locked? */
if ( $request->status->is_locked )
{
/* Figure out what we are doing */
switch( \IPS\Settings::i()->reply_request_resolved )
{
case 'reject': // The reply is rejected - let the customer know
$this->_processLocked( $email, $member, $request );
return TRUE;
break;
case 'create': // We are accepting the reply, however we are creating a new request from it rather than opening a new one.
$this->_processNewRequest( $email, $member, $department, $request );
return TRUE;
break;
}
}
/* Still here? Normal reply, or we are just re-opening the request regardless of resolved status - do that */
$this->_processReply( $email, $member, $request );
return TRUE;
}
/* Still here? Make sure the department is valid */
if ( !$department )
{
return FALSE;
}
/* Create request */
$this->_processNewRequest( $email, $member, $department );
return TRUE;
}
/**
* Is reply?
*
* @param \IPS\Email\Incoming\Email $email The email
* @return \IPS\nexus\Support\Request|NULL
* @note The key may come in as quoted printable
*/
protected function _emailIsReplyForRequest( \IPS\Email\Incoming\Email $email )
{
/* Figure out if this email belongs to an existing request */
if (
/* Look at the In-Reply-To header */
( isset( $email->headers['in-reply-to'] ) and preg_match( '/^<?IPS\-\d+\-SR(\d+)\.(.+?)(\.(\d+))?-/', $email->headers['in-reply-to'], $matches ) ) or
/* Look for [SRXX.XXX] in the raw content */
preg_match( '/\[SR(\d+?)\.(.+?)(\.(\d+))?\]/', $email->raw, $matches ) or
/* Look for the same in the message */
preg_match( '/\[SR(\d+?)\.(.+?)(\.(\d+))?\]/', $email->message, $matches ) or
/* If neither of those are found, look for the quoted printable version */
preg_match( '/\=5BSR(\d+?)\.(.+?)(\.(\d+))?\=5D/', $email->raw, $matches )
)
{
try
{
$request = \IPS\nexus\Support\Request::load( $matches[1] );
if ( $request->email_key === quoted_printable_decode( $matches[2] ) )
{
return $request;
}
}
catch ( \OutOfRangeException $e ) { }
}
/* If the same email generated a new support request within the last 10 minutes, add this as a reply to that */
try
{
$recentRequest = \IPS\Db::i()->select( '*', 'nexus_support_requests', array( 'r_email=? AND r_started>?', $email->from, ( time() - 600 ) ) )->first();
return \IPS\nexus\Support\Request::constructFromData( $recentRequest );
}
catch ( \UnderflowException $e ) { }
/* Still here? It must be a new thing */
return NULL;
}
/**
* Handle reply to locked request
*
* @param \IPS\Email\Incoming\Email $email The email
* @param \IPS\Member $member The member it's from
* @param \IPS|nexus\Support\Request $request The support request
* @return void
*/
protected function _processLocked( \IPS\Email\Incoming\Email $email, \IPS\Member $member, \IPS\nexus\Support\Request $request )
{
\IPS\Email::buildFromTemplate( 'nexus', 'replyRequestLocked', array( $request, $member ), \IPS\Email::TYPE_TRANSACTIONAL )->send( $member->member_id ? $member : $email->from );
}
/**
* Handle reply
*
* @param \IPS\Email\Incoming\Email $email The email
* @param \IPS\Member $member The member it's from
* @param \IPS\nexus\Support\Request $request The support request to make it a reply of
* @return void
*/
protected function _processReply( \IPS\Email\Incoming\Email $email, \IPS\Member $member, \IPS\nexus\Support\Request $request )
{
$staff = array_key_exists( $member->member_id, \IPS\nexus\Support\Request::staff() );
$pending = FALSE;
$newMessage = NULL;
if ( $staff and isset( $matches[4] ) )
{
$newMessage = $request->comments( 1, 0, 'date', 'desc' );
if ( $matches[4] != $newMessage->id )
{
$pending = TRUE;
}
}
$request->status = \IPS\nexus\Support\Status::load( TRUE, $staff ? 'status_default_staff' : 'status_default_member' );
$request->last_reply = time();
$request->last_reply_by = (int) $member->member_id;
$request->replies++;
$request->save();
$reply = new \IPS\nexus\Support\Reply;
$reply->request = $request->id;
$reply->member = (int) $member->member_id;
$reply->type = $staff ? ( $pending ? $reply::REPLY_PENDING : $reply::REPLY_STAFF ) : $reply::REPLY_EMAIL;
$reply->post = $email->message;
$reply->date = time();
$reply->email = $email->from;
$reply->cc = implode( ',', $email->cc );
$reply->raw = $email->raw;
$reply->save();
static::makeAndClaimAttachments( $email->attachments, $reply, $member );
if ( $pending )
{
$notifyEmail = \IPS\Email::buildFromTemplate( 'nexus', 'staffReplyPending', array( $reply, $newMessage ), \IPS\Email::TYPE_TRANSACTIONAL );
$notifyEmail->send( $member );
}
else
{
if ( $staff )
{
$defaultRecipients = $request->getDefaultRecipients();
$reply->sendCustomerNotifications( $defaultRecipients['to'], $defaultRecipients['cc'], $defaultRecipients['bcc'] );
}
$reply->sendNotifications();
}
}
/**
* Get department
*
* @param \IPS\Email\Incoming\Email $email The email
* @return \IPS\nexus\Support\Department
*/
protected function _getDepartment( \IPS\Email\Incoming\Email $email )
{
foreach ( $email->to as $to )
{
try
{
return \IPS\nexus\Support\Department::load( $to, 'dpt_email' );
}
catch ( \OutOfRangeException $e ) { }
}
return NULL;
}
/**
* Handle new requst
*
* @param \IPS\Email\Incoming\Email $email The email
* @param \IPS\Member $member The member it's from
* @param \IPS\nexus\Support\Department $department The department to put it in
* @param \IPS\nexus\Support\Request|NULL $request If this is from a previous request, the request, otherwise NULL
* @return void
*/
protected function _processNewRequest( \IPS\Email\Incoming\Email $email, \IPS\Member $member, \IPS\nexus\Support\Department $department, \IPS\nexus\Support\Request $previousRequest = NULL )
{
/* Make sure we're looking at the right department. */
if ( !is_null( $previousRequest ) )
{
/* Use the existing requests department instead. */
$department = $previousRequest->department;
}
/* Forwarded by a staff member? */
if (
preg_match( '/^FWD?: (.*)$/i', $email->subject, $matches ) // Was forwarded
and $member->member_id and array_key_exists( $member->member_id, \IPS\nexus\Support\Request::staff() ) // By a staff member
and $originallyFromEmail = $this->_originalSenderOfForwardedMessage( $email ) // And we can get the email address of the original sender
) {
list( $request, $reply ) = $this->_createRequestFromStaffForward( $email, $member, $department, $originallyFromEmail );
}
/* Nope, normal request */
else
{
list( $request, $reply ) = $this->_createNormalNewRequest( $email, $member, $department );
}
/* Send staff notifications */
$request->afterCreateLog( $reply );
if ( $previousRequest )
{
$request->log( 'previous_request', $previousRequest, NULL, $member );
$previousRequest->log( 'previous_request', NULL, $request, $member );
}
$request->sendNotifications();
}
/**
* Handle new request for an email that was forwarded by a staff member
*
* @param \IPS\Email\Incoming\Email $email The email
* @param \IPS\Member $member The member it's from
* @param \IPS\nexus\Support\Department $department The department to put it in
* @param string $originallyFromEmail The email address of the original sender
* @return [ \IPS\nexus\Support\Request, \IPS\nexus\Support\Reply ]
*/
protected function _createRequestFromStaffForward( \IPS\Email\Incoming\Email $email, \IPS\Member $member, \IPS\nexus\Support\Department $department, $originallyFromEmail )
{
/* Get the member for the original email */
$originallyFrom = \IPS\Member::load( $originallyFromEmail, 'email' );
/* Init request */
$request = $this->_initRequest( $member, $department );
preg_match( '/^FWD?: (.*)$/i', $email->subject, $matches );
$request->title = $matches[1];
$request->member = (int) $originallyFrom->member_id;
$request->email = $originallyFromEmail;
$request->save();
/* Strip any headers from the forwarded message */
$quoted = $email->quote;
$haveInitialHeader = FALSE;
$exploded = explode( '<br>', $quoted );
foreach ( $exploded as $k => $line )
{
if ( $line )
{
if ( !$haveInitialHeader and preg_match( '/^.*:/', $line ) )
{
$haveInitialHeader = TRUE;
}
elseif ( !preg_match( '/^.*:.*$/', $line ) )
{
$quoted = implode( '<br>', array_splice( $exploded, $k ) );
break;
}
}
}
$quoted = \IPS\Text\Parser::parseStatic( '<div>' . $quoted . '</div>' );
/* Create the reply */
$reply = $this->_createReply( $email, $request, $member, $quoted, $originallyFromEmail );
/* Create a hidden note */
if ( trim( $email->message ) or count( $email->attachments ) )
{
$note = new \IPS\nexus\Support\Reply;
$note->request = $request->id;
$note->member = (int) $member->member_id;
$note->type = $reply::REPLY_HIDDEN;
$note->post = $email->message;
$note->date = time();
$note->email = $email->from;
$note->save();
static::makeAndClaimAttachments( $email->attachments, $note, $member );
}
/* Return */
return array( $request, $reply );
}
/**
* Handle new request from a normal email
*
* @param \IPS\Email\Incoming\Email $email The email
* @param \IPS\Member $member The member it's from
* @param \IPS\nexus\Support\Department $department The department to put it in
* @return [ \IPS\nexus\Support\Request, \IPS\nexus\Support\Reply ]
*/
protected function _createNormalNewRequest( \IPS\Email\Incoming\Email $email, \IPS\Member $member, \IPS\nexus\Support\Department $department )
{
/* Init request */
$request = $this->_initRequest( $member, $department );
$request->title = $email->subject;
$request->member = (int) $member->member_id;
$request->email = $email->from;
/* Figure out who to notify for replies */
$notify = array();
foreach( $email->cc as $cc )
{
$notify[] = array( 'type' => 'e', 'value' => $cc );
}
foreach( $email->bcc as $cc )
{
$notify[] = array( 'type' => 'e', 'value' => $cc, 'bcc' => 1 );
}
$request->notify = $notify;
$request->save();
/* Create the reply */
$reply = $this->_createReply( $email, $request, $member, $email->message . $email->quote, $email->from );
static::makeAndClaimAttachments( $email->attachments, $reply, $member );
/* Send confirmation email */
if ( \IPS\Settings::i()->nexus_sout_autoreply )
{
$confirmationEmail = \IPS\Email::buildFromTemplate( 'nexus', 'emailConfirmation', array( $request ), \IPS\Email::TYPE_TRANSACTIONAL );
switch ( \IPS\Settings::i()->nexus_sout_from )
{
case 'staff':
case 'dpt':
$fromName = $member->language()->get( 'nexus_department_' . $request->department->_id );
break;
default:
$fromName = \IPS\Settings::i()->nexus_sout_from;
break;
}
$confirmationEmail->send( $member->member_id ? $member : $email->from, array(), array(), $request->department->email, $fromName, array( 'Message-Id' => "<IPS-0-SR{$request->id}.{$request->email_key}-{$request->department->email}>" ) );
}
/* Return */
return array( $request, $reply );
}
/**
* Create a basic support request object
*
* @param \IPS\Member $member The member it's from
* @param \IPS\nexus\Support\Department $department The department to put it in
* @return \IPS\nexus\Support\Request
*/
protected function _initRequest( \IPS\Member $member, \IPS\nexus\Support\Department $department )
{
$request = new \IPS\nexus\Support\Request;
$request->department = $department;
$request->status = \IPS\nexus\Support\Status::load( TRUE, 'status_default_member' );
$request->severity = \IPS\nexus\Support\Severity::load( TRUE, 'sev_default' );
$request->started = time();
$request->last_reply = time();
$request->last_reply_by = (int) $member->member_id;
$request->last_new_reply = time();
$request->replies = 1;
return $request;
}
/**
* Create a basic support reply object
*
* @param \IPS\Email\Incoming\Email $email The email
* @param \IPS\nexus\Support\Request $request The request it's for
* @param \IPS\Member $member The member it's from
* @param string $message The message body
* @param string $emailFrom The email address it is from
* @return \IPS\nexus\Support\Reply
*/
protected function _createReply( \IPS\Email\Incoming\Email $email, \IPS\nexus\Support\Request $request, \IPS\Member $member, $message, $emailFrom )
{
$reply = new \IPS\nexus\Support\Reply;
$reply->request = $request->id;
$reply->member = (int) $member->member_id;
$reply->type = $reply::REPLY_EMAIL;
$reply->post = $message;
$reply->date = time();
$reply->email = $emailFrom;
$reply->cc = implode( ',', $email->cc );
$reply->raw = $email->raw;
$reply->save();
return $reply;
}
/**
* If this message was forwarded by a staff member, get the
* email address it was sent to that staff member from (i.e.
* the user who should show as creating the support request)
*
* @param \IPS\Email\Incoming\Email $email The email
* @return bool
*/
protected function _originalSenderOfForwardedMessage( \IPS\Email\Incoming\Email $email )
{
$originallyFrom = NULL;
if ( isset( $email->headers['original-recipient'] ) )
{
$originallyFrom = preg_replace( '/^.*;(.*)$/', '$1', $email->headers['original-recipient'] );
}
else
{
if ( preg_match( '/From:.+?(\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b)/i', $email->quote, $_matches ) )
{
$originallyFrom = $_matches[1];
}
}
return $originallyFrom;
}
/**
* Make and claim attachments
*
* @param array $files \IPS\File objects
* @param \IPS\nexus\Support\Reply $reply The support request reply
* @param \IPS\Member $member The member it's from
* @return void
*/
public static function makeAndClaimAttachments( array $files, \IPS\nexus\Support\Reply $reply, \IPS\Member $member )
{
$replacements = array();
foreach ( $files as $file )
{
$attachment = $file->makeAttachment('', $member);
if ( !$file->isImage() )
{
$replacements["<fileStore.core_Attachment>/{$file}"] = \IPS\Settings::i()->base_url . "applications/core/interface/file/attachment.php?id={$attachment['attach_id']}";
}
\IPS\Db::i()->insert( 'core_attachments_map', array(
'attachment_id' => $attachment['attach_id'],
'location_key' => 'nexus_Support',
'id1' => $reply->request,
'id2' => $reply->id,
) );
}
if ( count( $replacements ) )
{
foreach ( $replacements as $find => $replace )
{
$reply->post = str_replace( $find, $replace, $reply->post );
}
$reply->save();
}
}
}