Link to a Mailbox the Customers automatically created when fetching e-mails
This commit is contained in:
parent
f07ecd9bdf
commit
c48f1472f9
4 changed files with 873 additions and 1 deletions
20
README.md
20
README.md
|
@ -25,7 +25,7 @@ composer require "millions-missing-france/freescout-restricted-customers" "0.5.0
|
|||
### Edit the application routes
|
||||
|
||||
This package does not seem to correctly override the routes of the main application.
|
||||
Overriding them has to be done manually, in the two following files.
|
||||
Overriding them has to be done manually, in the three following files.
|
||||
|
||||
#### routes/web.php
|
||||
|
||||
|
@ -137,6 +137,24 @@ should be replaced with:
|
|||
$html = __('Customers').' <a href="#" data-trigger="modal" data-modal-title="'.__('Add Customer').'" data-modal-size="lg" data-modal-no-footer="true" data-modal-body=\'<iframe src="'.route('freescout-restricted-customers.create_customer', ['x_embed' => 1]).'" frameborder="0" class="modal-iframe"></iframe>\' class="btn btn-bordered btn-xs" style="position:relative;top:-1px;margin-left:4px;"><i class="glyphicon glyphicon-plus" title="'.__('Add Customer').'" data-toggle="tooltip"></i></a>';
|
||||
```
|
||||
|
||||
### Edit the artisan commands
|
||||
|
||||
#### app/Console/Kernel.php
|
||||
|
||||
At the lines 107-108, this:
|
||||
|
||||
```php
|
||||
$fetch_command_identifier = \Helper::getWorkerIdentifier('freescout:fetch-emails');
|
||||
$fetch_command_name = 'freescout:fetch-emails'
|
||||
```
|
||||
|
||||
should be replaced with:
|
||||
|
||||
```php
|
||||
$fetch_command_identifier = \Helper::getWorkerIdentifier('freescout-restricted-customers:fetch-emails');
|
||||
$fetch_command_name = 'freescout-restricted-customers:fetch-emails'
|
||||
```
|
||||
|
||||
### Update the database schema
|
||||
|
||||
```
|
||||
|
|
771
src/Console/Commands/FetchEmails.php
Normal file
771
src/Console/Commands/FetchEmails.php
Normal file
|
@ -0,0 +1,771 @@
|
|||
<?php
|
||||
/*
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
SPDX-FileCopyrightText: © 2024 Millions Missing FRANCE <info@millionsmissing.fr>
|
||||
*/
|
||||
|
||||
namespace MillionsMissingFrance\FreescoutRestrictedCustomers\Console\Commands;
|
||||
|
||||
use App\Conversation;
|
||||
use MillionsMissingFrance\FreescoutRestrictedCustomers\Customer;
|
||||
use App\Events\CustomerCreatedConversation;
|
||||
use App\Events\CustomerReplied;
|
||||
use App\Thread;
|
||||
use app\Console\Commands\FetchEmails as BaseFetchEmails;
|
||||
|
||||
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
|
||||
*/
|
||||
protected $signature = 'freescout-restricted-customers:fetch-emails {--days=3} {--unseen=1} {--identifier=dummy} {--mailboxes=0}';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
if (\App\Email::sanitizeEmail($check_mailbox->email) == $recipient_email) {
|
||||
$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) {
|
||||
// Email belongs to mailbox
|
||||
// if (in_array(Email::sanitizeEmail($item->mail), $exclude_emails)) {
|
||||
// continue;
|
||||
// }
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,85 @@ class Customer extends BaseCustomer {
|
|||
return $this->belongsTo(Mailbox::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create customer or get existing and fill empty fields.
|
||||
*
|
||||
* @param string $email
|
||||
* @param array $data [description]
|
||||
*
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public static function create($email, $data = []) {
|
||||
$new = false;
|
||||
|
||||
$email = Email::sanitizeEmail($email);
|
||||
if (!$email) {
|
||||
return null;
|
||||
}
|
||||
$email_obj = Email::where('email', $email)->first();
|
||||
if ($email_obj) {
|
||||
$customer = $email_obj->customer;
|
||||
|
||||
// In case somehow the email has no customer.
|
||||
if (!$customer) {
|
||||
// Customer will be saved and connected to the email later.
|
||||
$customer = new self();
|
||||
}
|
||||
|
||||
// Update name if empty.
|
||||
/*if (empty($customer->first_name) && !empty($data['first_name'])) {
|
||||
$customer->first_name = $data['first_name'];
|
||||
if (empty($customer->last_name) && !empty($data['last_name'])) {
|
||||
$customer->last_name = $data['last_name'];
|
||||
}
|
||||
$customer->save();
|
||||
}*/
|
||||
} else {
|
||||
$customer = new self();
|
||||
$email_obj = new Email();
|
||||
$email_obj->email = $email;
|
||||
|
||||
$new = true;
|
||||
}
|
||||
// Ensure that the Mailbox relationship is set, if provided.
|
||||
if ( in_array('mailbox_id', $data) )
|
||||
$customer->mailbox_id = $data['mailbox_id'];
|
||||
|
||||
// Set empty fields
|
||||
if ($customer->setData($data, false) || !$customer->id) {
|
||||
$customer->save();
|
||||
}
|
||||
|
||||
if (empty($email_obj->id) || !$email_obj->customer_id || $email_obj->customer_id != $customer->id) {
|
||||
// Email may have been set in setData().
|
||||
$save_email = true;
|
||||
if (!empty($data['emails']) && is_array($data['emails'])) {
|
||||
foreach ($data['emails'] as $data_email) {
|
||||
if (is_string($data_email) && $data_email == $email) {
|
||||
$save_email = false;
|
||||
break;
|
||||
}
|
||||
if (is_array($data_email) && !empty($data_email['value']) && $data_email['value'] == $email) {
|
||||
$save_email = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($save_email) {
|
||||
$email_obj->customer()->associate($customer);
|
||||
$email_obj->save();
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: check phone uniqueness.
|
||||
|
||||
if ($new) {
|
||||
\Eventy::action('customer.created', $customer);
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set empty fields.
|
||||
*/
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace MillionsMissingFrance\FreescoutRestrictedCustomers;
|
|||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use MillionsMissingFrance\FreescoutRestrictedCustomers\Console\Commands\FetchEmails;
|
||||
|
||||
class FreescoutRestrictedCustomersServiceProvider extends ServiceProvider {
|
||||
public function register() {
|
||||
|
@ -18,6 +19,9 @@ class FreescoutRestrictedCustomersServiceProvider extends ServiceProvider {
|
|||
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
|
||||
$this->registerRoutes();
|
||||
$this->loadViewsFrom(__DIR__.'/../resources/views', 'freescout-restricted-customers');
|
||||
$this->commands([
|
||||
FetchEmails::class,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function registerRoutes() {
|
||||
|
|
Loading…
Reference in a new issue