Compare commits

..

2 commits

Author SHA1 Message Date
978a02c548
0.6.0 release
* Link to a Mailbox the Customers automatically created when fetching e-mails.
2024-07-09 18:45:38 +02:00
c48f1472f9
Link to a Mailbox the Customers automatically created when fetching e-mails 2024-07-09 18:44:50 +02:00
6 changed files with 879 additions and 3 deletions

View file

@ -1,3 +1,7 @@
0.6.0
* Link to a Mailbox the Customers automatically created when fetching e-mails.
0.5.0 0.5.0
* Allow multiple Customers to use the same email, as long as they are linked to distinct Mailboxes. * Allow multiple Customers to use the same email, as long as they are linked to distinct Mailboxes.

View file

@ -19,13 +19,13 @@ You have been warned.
### Install the package with composer ### Install the package with composer
``` ```
composer require "millions-missing-france/freescout-restricted-customers" "0.5.0" composer require "millions-missing-france/freescout-restricted-customers" "0.6.0"
``` ```
### Edit the application routes ### Edit the application routes
This package does not seem to correctly override the routes of the main application. 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 #### 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>'; $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 ### Update the database schema
``` ```

View file

@ -1,7 +1,7 @@
{ {
"name": "millions-missing-france/freescout-restricted-customers", "name": "millions-missing-france/freescout-restricted-customers",
"description": "Freescout restricted customers - Restrict access to Freescout customers to specific mailboxes", "description": "Freescout restricted customers - Restrict access to Freescout customers to specific mailboxes",
"version": "0.5.0", "version": "0.6.0",
"type": "library", "type": "library",
"license": ["AGPL-3.0-only"], "license": ["AGPL-3.0-only"],
"authors": [ "authors": [

View 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);
}
}
}

View file

@ -49,6 +49,85 @@ class Customer extends BaseCustomer {
return $this->belongsTo(Mailbox::class); 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. * Set empty fields.
*/ */

View file

@ -8,6 +8,7 @@ namespace MillionsMissingFrance\FreescoutRestrictedCustomers;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use MillionsMissingFrance\FreescoutRestrictedCustomers\Console\Commands\FetchEmails;
class FreescoutRestrictedCustomersServiceProvider extends ServiceProvider { class FreescoutRestrictedCustomersServiceProvider extends ServiceProvider {
public function register() { public function register() {
@ -18,6 +19,9 @@ class FreescoutRestrictedCustomersServiceProvider extends ServiceProvider {
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
$this->registerRoutes(); $this->registerRoutes();
$this->loadViewsFrom(__DIR__.'/../resources/views', 'freescout-restricted-customers'); $this->loadViewsFrom(__DIR__.'/../resources/views', 'freescout-restricted-customers');
$this->commands([
FetchEmails::class,
]);
} }
protected function registerRoutes() { protected function registerRoutes() {