2024-07-09 16:25:43 +00:00
< ? php
/*
SPDX - License - Identifier : AGPL - 3.0 - only
SPDX - FileCopyrightText : © 2024 Millions Missing FRANCE < info @ millionsmissing . fr >
*/
2024-07-10 15:16:13 +00:00
namespace Modules\MMFRestrictedCustomers\Console\Commands ;
2024-07-09 16:25:43 +00:00
2024-07-18 13:16:34 +00:00
use App\Attachment ;
2024-07-09 16:25:43 +00:00
use App\Conversation ;
2024-09-28 16:16:46 +00:00
use App\Email ;
use App\Thread ;
2024-07-09 16:25:43 +00:00
use App\Events\CustomerCreatedConversation ;
use App\Events\CustomerReplied ;
2024-09-28 16:16:46 +00:00
2024-07-18 13:16:34 +00:00
use App\Console\Commands\FetchEmails as BaseFetchEmails ;
use Modules\MMFRestrictedCustomers\Entities\Customer ;
2024-07-09 16:25:43 +00:00
class FetchEmails extends BaseFetchEmails {
/**
* The name and signature of the console command .
*
* -- identifier parameter is used to kill fetch - emails command running for too long .
*
* @ var string
*/
2024-07-10 15:16:13 +00:00
protected $signature = 'mmfrestrictedcustomers:fetch-emails {--days=3} {--unseen=1} {--identifier=dummy} {--mailboxes=0}' ;
2024-07-09 16:25:43 +00:00
/**
* Save email from customer as thread .
*/
public function saveCustomerThread ( $mailbox , $message_id , $prev_thread , $from , $to , $cc , $bcc , $subject , $body , $attachments , $headers , $date ) {
// Fetch date & time setting.
$use_mail_date_on_fetching = config ( 'app.use_mail_date_on_fetching' );
// Find conversation.
$new = false ;
$conversation = null ;
$prev_customer_id = null ;
if ( $use_mail_date_on_fetching ) {
$now = $date ;
} else {
$now = date ( 'Y-m-d H:i:s' );
}
$conv_cc = $cc ;
$prev_conv_cc = $conv_cc ;
// Customers are created before with email and name,
// and linked to a specific Mailbox.
$customer = Customer :: create ( $from , [ 'mailbox_id' => $mailbox -> id ]);
if ( $prev_thread ) {
$conversation = $prev_thread -> conversation ;
// If reply came from another customer: change customer, add original as CC.
// If FreeScout will not change the customer, the reply will be shown
// as coming from the original customer (not the real sender) and cause confusion.
// Below after events are fired we roll customer back.
if ( $conversation -> customer_id != $customer -> id ) {
$prev_customer_id = $conversation -> customer_id ;
$prev_customer_email = $conversation -> customer_email ;
// Do not add to CC emails from the original's BCC
if ( ! in_array ( $conversation -> customer_email , $conversation -> getBccArray ())) {
$conv_cc [] = $conversation -> customer_email ;
}
$conversation -> customer_id = $customer -> id ;
}
} else {
// Create conversation
$new = true ;
$conversation = new Conversation ();
$conversation -> type = Conversation :: TYPE_EMAIL ;
$conversation -> state = Conversation :: STATE_PUBLISHED ;
$conversation -> subject = $subject ;
$conversation -> setPreview ( $body );
$conversation -> mailbox_id = $mailbox -> id ;
$conversation -> customer_id = $customer -> id ;
$conversation -> created_by_customer_id = $customer -> id ;
$conversation -> source_via = Conversation :: PERSON_CUSTOMER ;
$conversation -> source_type = Conversation :: SOURCE_TYPE_EMAIL ;
$conversation -> created_at = $now ;
}
$prev_has_attachments = $conversation -> has_attachments ;
// Update has_attachments only if email has attachments AND conversation hasn't has_attachments already set
// Prevent to set has_attachments value back to 0 if the new reply doesn't have any attachment
if ( ! $conversation -> has_attachments && count ( $attachments )) {
// Later we will check which attachments are embedded.
$conversation -> has_attachments = true ;
}
// Save extra recipients to CC, but do not add the mailbox itself as a CC.
$conversation -> setCc ( array_merge ( $conv_cc , array_diff ( $to , $mailbox -> getEmails ())));
// BCC should keep BCC of the first email,
// so we change BCC only if it contains emails.
if ( $bcc ) {
$conversation -> setBcc ( $bcc );
}
$conversation -> customer_email = $from ;
// Reply from customer makes conversation active
if ( $conversation -> status != Conversation :: STATUS_ACTIVE ) {
$conversation -> status = \Eventy :: filter ( 'conversation.status_changing' , Conversation :: STATUS_ACTIVE , $conversation );
}
$conversation -> last_reply_at = $now ;
$conversation -> last_reply_from = Conversation :: PERSON_CUSTOMER ;
// Reply from customer to deleted conversation should undelete it.
if ( $conversation -> state == Conversation :: STATE_DELETED ) {
$conversation -> state = Conversation :: STATE_PUBLISHED ;
}
// Set folder id
$conversation -> updateFolder ();
$conversation -> save ();
// Thread
$thread = new Thread ();
$thread -> conversation_id = $conversation -> id ;
$thread -> user_id = $conversation -> user_id ;
$thread -> type = Thread :: TYPE_CUSTOMER ;
$thread -> status = $conversation -> status ;
$thread -> state = Thread :: STATE_PUBLISHED ;
$thread -> message_id = $message_id ;
$thread -> headers = $this -> headerToStr ( $headers );
$thread -> body = $body ;
$thread -> from = $from ;
$thread -> setTo ( $to );
$thread -> setCc ( $cc );
$thread -> setBcc ( $bcc );
$thread -> source_via = Thread :: PERSON_CUSTOMER ;
$thread -> source_type = Thread :: SOURCE_TYPE_EMAIL ;
$thread -> customer_id = $customer -> id ;
$thread -> created_by_customer_id = $customer -> id ;
$thread -> created_at = $now ;
$thread -> updated_at = $now ;
if ( $new ) {
$thread -> first = true ;
}
try {
$thread -> save ();
} catch ( \Exception $e ) {
// Could not save thread.
// https://github.com/freescout-helpdesk/freescout/issues/3186
if ( $new ) {
$conversation -> deleteForever ();
}
throw $e ;
}
$body_changed = false ;
$saved_attachments = $this -> saveAttachments ( $attachments , $thread -> id );
if ( $saved_attachments ) {
// After attachments saved to the disk we can replace cids in body (for PLAIN and HTML body)
$thread -> body = $this -> replaceCidsWithAttachmentUrls ( $thread -> body , $saved_attachments , $conversation , $prev_has_attachments );
$body_changed = true ;
foreach ( $saved_attachments as $saved_attachment ) {
if ( ! $saved_attachment [ 'attachment' ] -> embedded ) {
$thread -> has_attachments = true ;
break ;
}
}
}
$new_body = Thread :: replaceBase64ImagesWithAttachments ( $thread -> body );
if ( $new_body != $thread -> body ) {
$thread -> body = $new_body ;
$body_changed = true ;
}
if ( $body_changed ) {
$thread -> save ();
}
// Update conversation here if needed.
if ( $new ) {
$conversation = \Eventy :: filter ( 'conversation.created_by_customer' , $conversation , $thread , $customer );
} else {
$conversation = \Eventy :: filter ( 'conversation.customer_replied' , $conversation , $thread , $customer );
}
// save() will check if something in the model has changed. If it hasn't it won't run a db query.
$conversation -> save ();
// Update folders counters
$conversation -> mailbox -> updateFoldersCounters ();
if ( $new ) {
event ( new CustomerCreatedConversation ( $conversation , $thread ));
\Eventy :: action ( 'conversation.created_by_customer' , $conversation , $thread , $customer );
} else {
event ( new CustomerReplied ( $conversation , $thread ));
\Eventy :: action ( 'conversation.customer_replied' , $conversation , $thread , $customer );
}
// Conversation customer changed
// if ($prev_customer_id) {
// event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
// }
// Return original customer back.
if ( $prev_customer_id ) {
$conversation -> customer_id = $prev_customer_id ;
$conversation -> customer_email = $prev_customer_email ;
$conversation -> setCc ( array_merge ( $prev_conv_cc , array_diff ( $to , $mailbox -> getEmails ())));
$conversation -> save ();
}
return $thread ;
}
public function processMessage ( $message , $message_id , $mailbox , $mailboxes , $extra = false ) {
try {
// From - $from is the plain text email.
$from = $message -> getReplyTo ();
if ( ! $from
// https://github.com/freescout-helpdesk/freescout/issues/3101
|| ! ( $reply_to = $this -> formatEmailList ( $from ))
|| empty ( $reply_to [ 0 ])
|| preg_match ( '/^.+@unknown$/' , $reply_to [ 0 ])
) {
$from = $message -> getFrom ();
}
// https://github.com/freescout-helpdesk/freescout/issues/2833
/* else {
// If this is an auto-responder do not use Reply-To as sender email.
// https://github.com/freescout-helpdesk/freescout/issues/2826
$headers = $this -> headerToStr ( $message -> getHeader ());
if ( \MailHelper :: isAutoResponder ( $headers )) {
$from = $message -> getFrom ();
}
} */
if ( $from ) {
$from = $this -> formatEmailList ( $from );
}
if ( ! $from ) {
$this -> logError ( 'From is empty' );
$this -> setSeen ( $message , $mailbox );
return ;
} else {
$from = $from [ 0 ];
}
// Message-ID can be empty.
// https://stackoverflow.com/questions/8513165/php-imap-do-emails-have-to-have-a-messageid
if ( ! $message_id ) {
// Generate artificial Message-ID.
$message_id = \MailHelper :: generateMessageId ( $from , $message -> getRawBody ());
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Message-ID is empty, generated artificial Message-ID: ' . $message_id );
}
$duplicate_message_id = false ;
// Special hack to allow threading into conversations Jira messages.
// https://github.com/freescout-helpdesk/freescout/issues/2927
//
// Jira does not properly populate Reference / In-Reply-To headers.
// When Jira sends a reply the In-Reply-To header is set to:
// JIRA.$\{issue-id}.$\{issue-created-date-millis}@$\{host}
//
// If we see the first message of a ticket we change the Message-ID,
// so all follow-ups in the ticket are nicely threaded.
$jira_message_id = preg_replace ( '/^(JIRA\.\d+\.\d+)\..*(@Atlassian.JIRA)/' , '\1\2' , $message_id );
if ( $jira_message_id != $message_id ) {
if ( ! Thread :: where ( 'message_id' , $jira_message_id ) -> exists ()) {
$message_id = $jira_message_id ;
}
}
if ( ! $extra ) {
$duplicate_message_id = Thread :: where ( 'message_id' , $message_id ) -> first ();
}
// Mailbox has been mentioned in Bcc.
if ( ! $extra && $duplicate_message_id ) {
$recipients = array_merge (
$this -> formatEmailList ( $message -> getTo ()),
$this -> formatEmailList ( $message -> getCc ())
);
if ( ! in_array ( Email :: sanitizeEmail ( $mailbox -> email ), $recipients )
// Make sure that previous email has been imported into other mailbox.
&& $duplicate_message_id -> conversation
&& $duplicate_message_id -> conversation -> mailbox_id != $mailbox -> id
) {
$extra = true ;
$duplicate_message_id = null ;
}
}
// Gnerate artificial Message-ID if importing same email into several mailboxes.
if ( $extra ) {
// Generate artificial Message-ID.
$message_id = \MailHelper :: generateMessageId ( strstr ( $message_id , '@' ) ? $message_id : $from , $mailbox -> id . $message_id );
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Generated artificial Message-ID: ' . $message_id );
}
// Check if message already fetched.
if ( $duplicate_message_id ) {
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Message with such Message-ID has been fetched before: ' . $message_id );
$this -> setSeen ( $message , $mailbox );
return ;
}
// Detect prev thread
$is_reply = false ;
$prev_thread = null ;
$user_id = null ;
$user = null ; // for user reply only
$message_from_customer = true ;
$in_reply_to = $message -> getInReplyTo ();
$references = $message -> getReferences ();
$attachments = $message -> getAttachments ();
$html_body = '' ;
// Is it a bounce message
$is_bounce = false ;
// Determine previous Message-ID
$prev_message_id = '' ;
if ( $in_reply_to ) {
$prev_message_id = trim ( $in_reply_to , '<>' );
} elseif ( $references ) {
if ( ! is_array ( $references )) {
$references = array_filter ( preg_split ( '/[, <>]/' , $references ));
}
// Find first non-empty reference
if ( is_array ( $references )) {
foreach ( $references as $reference ) {
if ( ! empty ( trim ( $reference ))) {
$prev_message_id = trim ( $reference );
break ;
}
}
}
}
// Some mail service providers change Message-ID of the outgoing email,
// so we are passing Message-ID in marker in body.
$reply_prefixes = [
\MailHelper :: MESSAGE_ID_PREFIX_NOTIFICATION ,
\MailHelper :: MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER ,
\MailHelper :: MESSAGE_ID_PREFIX_AUTO_REPLY ,
];
// Try to get previous message ID from marker in body.
if ( ! $prev_message_id || ! preg_match ( '/^(' . implode ( '|' , $reply_prefixes ) . ')\-(\d+)\-/' , $prev_message_id )) {
$html_body = $message -> getHTMLBody ( false );
$marker_message_id = \MailHelper :: fetchMessageMarkerValue ( $html_body );
if ( $marker_message_id ) {
$prev_message_id = $marker_message_id ;
}
}
// Bounce detection.
$bounced_message_id = null ;
if ( $message -> hasAttachments ()) {
// Detect bounce by attachment.
// Check all attachments.
foreach ( $attachments as $attachment ) {
if ( ! empty ( Attachment :: $types [ $attachment -> getType ()]) && Attachment :: $types [ $attachment -> getType ()] == Attachment :: TYPE_MESSAGE
) {
if (
// Checking the name will lead to mistakes if someone attaches a file with such name.
// Dashes are converted to space.
//in_array(strtoupper($attachment->getName()), ['RFC822', 'DELIVERY STATUS', 'DELIVERY STATUS NOTIFICATION', 'UNDELIVERED MESSAGE'])
preg_match ( '/delivery-status/' , strtolower ( $attachment -> content_type ))
// 7.3.1 The Message/rfc822 (primary) subtype. A Content-Type of "message/rfc822" indicates that the body contains an encapsulated message, with the syntax of an RFC 822 message
//|| $attachment->content_type == 'message/rfc822'
) {
$is_bounce = true ;
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Bounce detected by attachment content-type: ' . $attachment -> content_type );
// Try to get Message-ID of the original email.
if ( ! $bounced_message_id ) {
//print_r(\MailHelper::parseHeaders($attachment->getContent()));
$bounced_message_id = \MailHelper :: getHeader ( $attachment -> getContent (), 'message_id' );
}
}
}
}
}
$message_header = $this -> headerToStr ( $message -> getHeader ());
// Check Content-Type header.
if ( ! $is_bounce && $message_header ) {
if ( \MailHelper :: detectBounceByHeaders ( $message_header )) {
$is_bounce = true ;
}
}
// Check message's From field.
if ( ! $is_bounce ) {
if ( $message -> getFrom ()) {
$original_from = $this -> formatEmailList ( $message -> getFrom ());
$original_from = $original_from [ 0 ];
$is_bounce = preg_match ( '/^mailer\-daemon@/i' , $original_from );
if ( $is_bounce ) {
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Bounce detected by From header: ' . $original_from );
}
}
}
// Check Return-Path header
if ( ! $is_bounce && preg_match ( " /^Return \ -Path: <>/i " , $message_header )) {
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Bounce detected by Return-Path header.' );
$is_bounce = true ;
}
if ( $is_bounce && ! $bounced_message_id ) {
foreach ( $attachments as $attachment_msg ) {
// 7.3.1 The Message/rfc822 (primary) subtype. A Content-Type of "message/rfc822" indicates that the body contains an encapsulated message, with the syntax of an RFC 822 message
if ( $attachment_msg -> content_type == 'message/rfc822' ) {
$bounced_message_id = \MailHelper :: getHeader ( $attachment_msg -> getContent (), 'message_id' );
if ( $bounced_message_id ) {
break ;
}
}
}
}
// Is it a message from Customer or User replied to the notification
preg_match ( '/^' . \MailHelper :: MESSAGE_ID_PREFIX_NOTIFICATION . " \ -( \ d+) \ -( \ d+) \ -/ " , $prev_message_id , $m );
if ( ! $is_bounce && ! empty ( $m [ 1 ]) && ! empty ( $m [ 2 ])) {
// Reply from User to the notification
$prev_thread = Thread :: find ( $m [ 1 ]);
$user_id = $m [ 2 ];
$user = User :: find ( $user_id );
$message_from_customer = false ;
$is_reply = true ;
if ( ! $user ) {
$this -> logError ( 'User not found: ' . $user_id );
$this -> setSeen ( $message , $mailbox );
return ;
}
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Message from: User' );
} else {
// Message from Customer or User replied to his reply to notification
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Message from: Customer' );
if ( ! $is_bounce ) {
if ( $prev_message_id ) {
$prev_thread_id = '' ;
// Customer replied to the email from user
preg_match ( '/^' . \MailHelper :: MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER . " \ -( \ d+) \ -([a-z0-9]+)@/ " , $prev_message_id , $m );
// Simply checking thread_id from message_id was causing an issue when
// customer was sending a message from FreeScout - the message was
// connected to the wrong conversation.
if ( ! empty ( $m [ 1 ]) && ! empty ( $m [ 2 ])) {
$message_id_hash = $m [ 2 ];
if ( strlen ( $message_id_hash ) == 16 ) {
if ( $message_id_hash == \MailHelper :: getMessageIdHash ( $m [ 1 ])) {
$prev_thread_id = $m [ 1 ];
}
} else {
// Backward compatibility.
$prev_thread_id = $m [ 1 ];
}
}
// Customer replied to the auto reply
if ( ! $prev_thread_id ) {
preg_match ( '/^' . \MailHelper :: MESSAGE_ID_PREFIX_AUTO_REPLY . " \ -( \ d+) \ -([a-z0-9]+)@/ " , $prev_message_id , $m );
if ( ! empty ( $m [ 1 ]) && ! empty ( $m [ 2 ])) {
$message_id_hash = $m [ 2 ];
if ( strlen ( $message_id_hash ) == 16 ) {
if ( $message_id_hash == \MailHelper :: getMessageIdHash ( $m [ 1 ])) {
$prev_thread_id = $m [ 1 ];
}
} else {
// Backward compatibility.
$prev_thread_id = $m [ 1 ];
}
}
}
if ( $prev_thread_id ) {
$prev_thread = Thread :: find ( $prev_thread_id );
} else {
// Customer replied to his own message
$prev_thread = Thread :: where ( 'message_id' , $prev_message_id ) -> first ();
}
// Reply from user to his reply to the notification
if ( ! $prev_thread
&& ( $prev_thread = Thread :: where ( 'message_id' , $prev_message_id ) -> first ())
&& $prev_thread -> created_by_user_id
&& $prev_thread -> created_by_user -> hasEmail ( $from )
) {
$user_id = $user -> id ;
$message_from_customer = false ;
$is_reply = true ;
}
}
if ( ! empty ( $prev_thread )) {
$is_reply = true ;
}
}
}
// Make sure that prev_thread belongs to the current mailbox.
// Problems may arise when forwarding conversation for example.
//
// For replies to email notifications it's allowed to have prev_thread in
// another mailbox as conversation can be moved.
// https://github.com/freescout-helpdesk/freescout/issues/3455
if ( $prev_thread && $message_from_customer ) {
if ( $prev_thread -> conversation -> mailbox_id != $mailbox -> id ) {
// https://github.com/freescout-helpdesk/freescout/issues/2807
// Behaviour of email sent to multiple mailboxes:
// If a user from either mailbox replies, then a new conversation is created
// in the other mailbox with another new conversation ID.
//
// Try to get thread by generated message ID.
if ( $in_reply_to ) {
$prev_thread = Thread :: where ( 'message_id' , \MailHelper :: generateMessageId ( $in_reply_to , $mailbox -> id . $in_reply_to )) -> first ();
if ( ! $prev_thread ) {
$prev_thread = null ;
$is_reply = false ;
}
} else {
$prev_thread = null ;
$is_reply = false ;
}
}
}
// Get body
if ( ! $html_body ) {
// Get body and do not replace :cid with images base64
$html_body = $message -> getHTMLBody ( false );
}
$is_html = true ;
if ( $html_body ) {
$body = $html_body ;
} else {
$is_html = false ;
$body = $message -> getTextBody () ? ? '' ;
$body = htmlspecialchars ( $body );
}
$body = $this -> separateReply ( $body , $is_html , $is_reply , ! $message_from_customer , (( $message_from_customer && $prev_thread ) ? $prev_thread -> getMessageId ( $mailbox ) : '' ));
// We have to fetch absolutely all emails, even with empty body.
// if (!$body) {
// $this->logError('Message body is empty');
// $this->setSeen($message, $mailbox);
// continue;
// }
// Webklex/php-imap returns object instead of a string.
$subject = $message -> getSubject () . " " ;
// Convert subject encoding
if ( preg_match ( '/=\?[a-z\d-]+\?[BQ]\?.*\?=/i' , $subject )) {
$subject = iconv_mime_decode ( $subject , ICONV_MIME_DECODE_CONTINUE_ON_ERROR , 'UTF-8' );
}
$to = $this -> formatEmailList ( $message -> getTo ());
$cc = $this -> formatEmailList ( $message -> getCc ());
// It will always return an empty value as it's Bcc.
$bcc = $this -> formatEmailList ( $message -> getBcc ());
// If existing user forwarded customer's email to the mailbox
// we are creating a new conversation as if it was sent by the customer.
if ( $in_reply_to
// We should use body here, as entire HTML may contain
// email looking things.
//&& ($fwd_body = $html_body ?: $message->getTextBody())
&& $body
//&& preg_match("/^(".implode('|', \MailHelper::$fwd_prefixes)."):(.*)/i", $subject, $m)
// F:, FW:, FWD:, WG:, De:
&& preg_match ( " /^[[:alpha:]] { 1,3}:(.*)/i " , $subject , $m )
// It can be just "Fwd:"
//&& !empty($m[1])
&& ! $user_id && ! $is_reply && ! $prev_thread
// Only if the email has been sent to one mailbox.
&& count ( $to ) == 1 && count ( $cc ) == 0
&& preg_match ( " /^[ \ s]* " . self :: FWD_AS_CUSTOMER_COMMAND . " /su " , trim ( strip_tags ( $body )))
) {
// Try to get "From:" from body.
$original_sender = $this -> getOriginalSenderFromFwd ( $body );
if ( $original_sender ) {
// Check if sender is the existing user.
$sender_is_user = User :: nonDeleted () -> where ( 'email' , $from ) -> exists ();
if ( $sender_is_user ) {
// Substitute sender.
$from = $original_sender ;
$subject = trim ( $m [ 1 ] ? ? $subject );
$message_from_customer = true ;
// Remove @fwd from body.
$body = trim ( preg_replace ( " / " . self :: FWD_AS_CUSTOMER_COMMAND . " ([ \ s<]+)/su " , '$1' , $body ));
}
}
}
// Create customers
$emails = array_merge (
$this -> attrToArray ( $message -> getFrom ()),
$this -> attrToArray ( $message -> getReplyTo ()),
$this -> attrToArray ( $message -> getTo ()),
$this -> attrToArray ( $message -> getCc ()),
// It will always return an empty value as it's Bcc.
$this -> attrToArray ( $message -> getBcc ())
);
// We need the createCustomers method to be aware of the Mailbox currently in use.
$this -> createCustomers ( $emails , $mailbox );
$date = $this -> attrToDate ( $message -> getDate ());
if ( $date ) {
$app_timezone = config ( 'app.timezone' );
if ( $app_timezone ) {
$date -> setTimezone ( $app_timezone );
}
}
$now = now ();
if ( ! $date || $date -> greaterThan ( $now )) {
$date = $now ;
}
$data = \Eventy :: filter ( 'fetch_emails.data_to_save' , [
'mailbox' => $mailbox ,
'message_id' => $message_id ,
'prev_thread' => $prev_thread ,
'from' => $from ,
'to' => $to ,
'cc' => $cc ,
'bcc' => $bcc ,
'subject' => $subject ,
'body' => $body ,
'attachments' => $attachments ,
'message' => $message ,
'is_bounce' => $is_bounce ,
'message_from_customer' => $message_from_customer ,
'user' => $user ,
'date' => $date ,
]);
$new_thread = null ;
if ( $message_from_customer ) {
// We should import the message into other mailboxes even if previous thread is set.
// https://github.com/freescout-helpdesk/freescout/issues/3473
//if (!$data['prev_thread']) {
// Maybe this email need to be imported also into other mailbox.
$recipient_emails = array_unique ( $this -> formatEmailList ( array_merge (
$this -> attrToArray ( $message -> getTo ()),
$this -> attrToArray ( $message -> getCc ()),
// It will always return an empty value as it's Bcc.
$this -> attrToArray ( $message -> getBcc ())
)));
if ( count ( $mailboxes ) && count ( $recipient_emails ) > 1 ) {
foreach ( $mailboxes as $check_mailbox ) {
if ( $check_mailbox -> id == $mailbox -> id ) {
continue ;
}
if ( ! $check_mailbox -> isInActive ()) {
continue ;
}
foreach ( $recipient_emails as $recipient_email ) {
// No need to check mailbox aliases.
2024-09-28 16:16:46 +00:00
if ( Email :: sanitizeEmail ( $check_mailbox -> email ) == $recipient_email ) {
2024-07-09 16:25:43 +00:00
$this -> extra_import [] = [
'mailbox' => $check_mailbox ,
'message' => $message ,
'message_id' => $message_id ,
];
break ;
}
}
}
}
//}
if ( \Eventy :: filter ( 'fetch_emails.should_save_thread' , true , $data ) !== false ) {
// SendAutoReply listener will check bounce flag and will not send an auto reply if this is an auto responder.
$new_thread = $this -> saveCustomerThread ( $mailbox , $data [ 'message_id' ], $data [ 'prev_thread' ], $data [ 'from' ], $data [ 'to' ], $data [ 'cc' ], $data [ 'bcc' ], $data [ 'subject' ], $data [ 'body' ], $data [ 'attachments' ], $data [ 'message' ] -> getHeader (), $data [ 'date' ]);
} else {
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Hook fetch_emails.should_save_thread returned false. Skipping message.' );
$this -> setSeen ( $message , $mailbox );
return ;
}
} else {
// Check if From is the same as user's email.
// If not we send an email with information to the sender.
if ( ! $user -> hasEmail ( $from )) {
$this -> logError ( " Sender address { $from } does not match " . $user -> getFullName () . " user email: " . $user -> email . " . Add " . $user -> email . " to user's Alternate Emails in the users's profile to allow the user reply from this address. " );
$this -> setSeen ( $message , $mailbox );
// Send "Unable to process your update email" to user
\App\Jobs\SendEmailReplyError :: dispatch ( $from , $user , $mailbox ) -> onQueue ( 'emails' );
return ;
}
// Save user thread only if there prev_thread is set.
// https://github.com/freescout-helpdesk/freescout/issues/3455
if ( ! $prev_thread ) {
$this -> logError ( " Support agent's reply to the email notification could not be processed as previous thread could not be determined. " );
$this -> setSeen ( $message , $mailbox );
return ;
}
if ( \Eventy :: filter ( 'fetch_emails.should_save_thread' , true , $data ) !== false ) {
$new_thread = $this -> saveUserThread ( $data [ 'mailbox' ], $data [ 'message_id' ], $data [ 'prev_thread' ], $data [ 'user' ], $data [ 'from' ], $data [ 'to' ], $data [ 'cc' ], $data [ 'bcc' ], $data [ 'body' ], $data [ 'attachments' ], $data [ 'message' ] -> getHeader (), $data [ 'date' ]);
} else {
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Hook fetch_emails.should_save_thread returned false. Skipping message.' );
$this -> setSeen ( $message , $mailbox );
return ;
}
}
if ( $new_thread ) {
$this -> setSeen ( $message , $mailbox );
$this -> line ( '[' . date ( 'Y-m-d H:i:s' ) . '] Thread successfully created: ' . $new_thread -> id );
// If it was a bounce message, save bounce data.
if ( $message_from_customer && $is_bounce ) {
$this -> saveBounceData ( $new_thread , $bounced_message_id , $from );
}
} else {
$this -> logError ( 'Error occurred processing message' );
}
} catch ( \Exception $e ) {
$this -> setSeen ( $message , $mailbox );
$this -> logError ( \Helper :: formatException ( $e ));
}
}
/**
* Create customers from emails .
*
* @ param array $emails_data
*/
public function createCustomers ( $emails , $mailbox ) {
$exclude_emails = $mailbox -> getEmails ();
foreach ( $emails as $item ) {
$data = [
'mailbox_id' => $mailbox -> id ,
];
if ( ! empty ( $item -> personal )) {
$name_parts = explode ( ' ' , $item -> personal , 2 );
$data [ 'first_name' ] = $name_parts [ 0 ];
if ( ! empty ( $name_parts [ 1 ])) {
$data [ 'last_name' ] = $name_parts [ 1 ];
}
}
Customer :: create ( $item -> mail , $data );
}
}
}