1
0
mirror of https://github.com/freescout-helpdesk/freescout.git synced 2024-11-24 03:12:46 +01:00

View send log for threads

This commit is contained in:
FreeScout 2018-08-20 10:41:51 -07:00
parent 2016ef2a53
commit a6cd30caa1
10 changed files with 261 additions and 33 deletions

View File

@ -13,8 +13,10 @@ use App\Events\UserReplied;
use App\Folder; use App\Folder;
use App\Mailbox; use App\Mailbox;
use App\MailboxUser; use App\MailboxUser;
use App\SendLog;
use App\Thread; use App\Thread;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Input;
use Validator; use Validator;
class ConversationsController extends Controller class ConversationsController extends Controller
@ -512,6 +514,63 @@ class ConversationsController extends Controller
return \Response::json($response); return \Response::json($response);
} }
/**
* Conversations ajax controller.
*/
public function ajaxHtml(Request $request)
{
switch ($request->action) {
case 'send_log':
return $this->ajaxHtmlSendLog();
}
abort(404);
}
/**
* Send log
* @return [type] [description]
*/
public function ajaxHtmlSendLog()
{
$thread_id = Input::get('thread_id');
if (!$thread_id) {
abort(404);
}
$thread = Thread::find($thread_id);
if (!$thread) {
abort(404);
}
$user = auth()->user();
if (!$user->can('view', $thread->conversation)) {
abort(403);
}
// Get send log
$log_records = SendLog::where('thread_id', $thread_id)
->orderBy('created_at', 'desc')
->get();
$customers_log = [];
$users_log = [];
foreach ($log_records as $log_record) {
if ($log_record->user_id) {
$users_log[$log_record->email][] = $log_record;
} else {
$customers_log[$log_record->email][] = $log_record;
}
}
return view('conversations/ajax_html/send_log', [
'customers_log' => $customers_log,
'users_log' => $users_log,
]);
}
/** /**
* Get redirect URL after performing an action. * Get redirect URL after performing an action.
*/ */

View File

@ -22,6 +22,11 @@ class SendReplyToCustomer implements ShouldQueue
public $customer; public $customer;
private $failures = [];
private $recipients = [];
private $last_thread = null;
private $message_id = '';
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -50,11 +55,11 @@ class SendReplyToCustomer implements ShouldQueue
$new = false; $new = false;
$headers = []; $headers = [];
$last_thread = $this->threads->first(); $this->last_thread = $this->threads->first();
$prev_thread = null; $prev_thread = null;
// Configure mail driver according to Mailbox settings // Configure mail driver according to Mailbox settings
\App\Mail\Mail::setMailDriver($mailbox, $last_thread->created_by_user); \App\Mail\Mail::setMailDriver($mailbox, $this->last_thread->created_by_user);
if (count($this->threads) == 1) { if (count($this->threads) == 1) {
$new = true; $new = true;
@ -73,15 +78,16 @@ class SendReplyToCustomer implements ShouldQueue
$headers['In-Reply-To'] = '<'.$prev_thread->message_id.'>'; $headers['In-Reply-To'] = '<'.$prev_thread->message_id.'>';
$headers['References'] = '<'.$prev_thread->message_id.'>'; $headers['References'] = '<'.$prev_thread->message_id.'>';
} }
$message_id = \App\Mail\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$last_thread->id.'-'.md5($last_thread->id).'@'.$mailbox->getEmailDomain(); $this->message_id = \App\Mail\Mail::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER.'-'.$this->last_thread->id.'-'.md5($this->last_thread->id).'@'.$mailbox->getEmailDomain();
$headers['Message-ID'] = $message_id; $headers['Message-ID'] = $this->message_id;
$customer_email = $this->customer->getMainEmail(); $customer_email = $this->customer->getMainEmail();
$cc_array = $mailbox->removeMailboxEmailsFromList($last_thread->getCcArray()); $cc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getCcArray());
$bcc_array = $mailbox->removeMailboxEmailsFromList($last_thread->getBccArray()); $bcc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getBccArray());
$this->recipients = array_merge([$customer_email], $cc_array, $bcc_array);
try { try {
$mail = Mail::to([['name' => $this->customer->getFullName(), 'email' => $customer_email]]) Mail::to([['name' => $this->customer->getFullName(), 'email' => $customer_email]])
->cc($cc_array) ->cc($cc_array)
->bcc($bcc_array) ->bcc($bcc_array)
->send(new ReplyToCustomer($this->conversation, $this->threads, $headers)); ->send(new ReplyToCustomer($this->conversation, $this->threads, $headers));
@ -93,31 +99,23 @@ class SendReplyToCustomer implements ShouldQueue
]) ])
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING) ->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR); ->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR);
// Failures will be save to send log when retry attempts will finish
$this->failures = $this->recipients;
throw $e;
} }
// In message_id we are storing Message-ID of the incoming email which created the thread // In message_id we are storing Message-ID of the incoming email which created the thread
// Outcoming message_id can be generated for each thread by thread->id // Outcoming message_id can be generated for each thread by thread->id
// $last_thread->message_id = $message_id; // $this->last_thread->message_id = $message_id;
// $last_thread->save(); // $this->last_thread->save();
// Laravel tells us exactly what email addresses failed // Laravel tells us exactly what email addresses failed
$failures = Mail::failures(); $this->failures = Mail::failures();
// Save to send log // Save to send log
$recipients = array_merge([$customer_email], $cc_array, $bcc_array); $this->saveToSendLog();
foreach ($recipients as $recipient) {
if (in_array($recipient, $failures)) {
$status = SendLog::STATUS_SEND_ERROR;
} else {
$status = SendLog::STATUS_ACCEPTED;
}
if ($customer_email == $recipient) {
$customer_id = $this->customer->id;
} else {
$customer_id = null;
}
SendLog::log($last_thread->id, $message_id, $recipient, $status, $customer_id);
}
if (!empty($failures)) { if (!empty($failures)) {
throw new \Exception('Could not send email to: '.implode(', ', $failures)); throw new \Exception('Could not send email to: '.implode(', ', $failures));
@ -142,5 +140,27 @@ class SendReplyToCustomer implements ShouldQueue
]) ])
->useLog(\App\ActivityLog::NAME_EMAILS_SENDING) ->useLog(\App\ActivityLog::NAME_EMAILS_SENDING)
->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR); ->log(\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR);
$this->saveToSendLog();
}
/**
* Save failed email to send log.
*/
public function saveToSendLog()
{
foreach ($this->recipients as $recipient) {
if (in_array($recipient, $this->failures)) {
$status = SendLog::STATUS_SEND_ERROR;
} else {
$status = SendLog::STATUS_ACCEPTED;
}
if ($customer_email == $recipient) {
$customer_id = $this->customer->id;
} else {
$customer_id = null;
}
SendLog::log($this->last_thread->id, $this->message_id, $recipient, $status, $customer_id);
}
} }
} }

View File

@ -0,0 +1,20 @@
<?php
namespace App\Observers;
class SendLogObserver
{
/**
* Send log created.
*
* @param SendLog $send_log
*/
public function created(SendLog $send_log)
{
// Update status for thread if any
if ($send_log->thread_id && ($send_log->customer_id || ($send_log->user_id && $send_log->user_id == $send_log->thread->user_id)) {
$send_log->thread->send_status = $send_log->status;
$send_log->thread->save();
}
}
}

View File

@ -56,7 +56,7 @@ class SendLog extends Model
/** /**
* Save log record. * Save log record.
*/ */
public static function log($thread_id, $message_id, $email, $status, $customer_id = null, $user_id = null, $message = null) public static function log($thread_id, $message_id, $email, $status, $customer_id = null, $user_id = null, $status_message = null)
{ {
$send_log = new self(); $send_log = new self();
$send_log->thread_id = $thread_id; $send_log->thread_id = $thread_id;
@ -65,9 +65,54 @@ class SendLog extends Model
$send_log->status = $status; $send_log->status = $status;
$send_log->customer_id = $customer_id; $send_log->customer_id = $customer_id;
$send_log->user_id = $user_id; $send_log->user_id = $user_id;
$send_log->message = $message; $send_log->status_message = $status_message;
$send_log->save(); $send_log->save();
return true; return true;
} }
/**
* Get name of the status.
*/
public function getStatusName()
{
switch ($this->status) {
case self::STATUS_ACCEPTED:
return __('Accepted for delivery');
case self::STATUS_SEND_ERROR:
return __('Send error');
case self::STATUS_DELIVERY_SUCCESS:
return __('Successfully delivered');
case self::STATUS_DELIVERY_ERROR:
return __('Delivery error');
case self::STATUS_OPENED:
return __('Recipient opened the message');
case self::STATUS_CLICKED:
return __('Recipient clicked a link in the message');
case self::STATUS_UNSUBSCRIBED:
return __('Recipient unsubscribed');
case self::STATUS_COMPLAINED:
return __('Recipient complained');
default:
return __('Unknown');
}
}
public function isErrorStatus()
{
if (in_array($this->status, [self::STATUS_SEND_ERROR, self::STATUS_DELIVERY_ERROR])) {
return true;
} else {
return false;
}
}
public function isSuccessStatus()
{
if (in_array($this->status, [self::STATUS_DELIVERY_SUCCESS])) {
return true;
} else {
return false;
}
}
} }

View File

@ -51,8 +51,9 @@ class CreateThreadsTable extends Migration
$table->integer('created_by_customer_id')->nullable(); $table->integer('created_by_customer_id')->nullable();
// ID of Saved reply that was used to create this Thread (savedReplyId) // ID of Saved reply that was used to create this Thread (savedReplyId)
$table->integer('saved_reply_id')->nullable(); $table->integer('saved_reply_id')->nullable();
// Status of the email sent to customer or user, to whom the thread is assigned // Status of the email sent to the customer or user, to whom the thread is assigned
//$table->unsignedTinyInteger('send_status')->default(Thread::SEND_STATUS_TOSEND); // Values are in SendLog
$table->unsignedTinyInteger('send_status')->nullable();
// Text describing the sending status // Text describing the sending status
//$table->string('send_status_text', 255)->nullable(); //$table->string('send_status_text', 255)->nullable();
// Email opened by customer // Email opened by customer

View File

@ -2,9 +2,9 @@
/** /**
* Outgoing emails. * Outgoing emails.
*/ */
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSendLogsTable extends Migration class CreateSendLogsTable extends Migration
{ {
@ -18,6 +18,7 @@ class CreateSendLogsTable extends Migration
Schema::create('send_logs', function (Blueprint $table) { Schema::create('send_logs', function (Blueprint $table) {
$table->increments('id'); $table->increments('id');
$table->integer('thread_id')->index(); $table->integer('thread_id')->index();
// Customer ID is set only if email sent to the main conversation customer
$table->integer('customer_id')->nullable(); $table->integer('customer_id')->nullable();
$table->integer('user_id')->nullable(); $table->integer('user_id')->nullable();
// Message-ID header of the outgoing email // Message-ID header of the outgoing email
@ -25,7 +26,7 @@ class CreateSendLogsTable extends Migration
// We have to keep email as customer's or user's email may change // We have to keep email as customer's or user's email may change
$table->string('email', 191); $table->string('email', 191);
$table->unsignedTinyInteger('status'); $table->unsignedTinyInteger('status');
$table->string('message', 255)->nullable(); $table->string('status_message', 255)->nullable();
$table->timestamps(); $table->timestamps();
// Indexes // Indexes

18
public/js/main.js vendored
View File

@ -132,6 +132,12 @@ $(document).ready(function(){
$.summernote.lang['en-US'].image.dropImage = Lang.get("messages.drag_image_file"); $.summernote.lang['en-US'].image.dropImage = Lang.get("messages.drag_image_file");
} }
})(jQuery); })(jQuery);
// Modal windows
$('a[data-trigger="modal"]').click(function(e) {
showModal($(this));
e.preventDefault();
});
}); });
function mailboxUpdateInit(from_name_custom) function mailboxUpdateInit(from_name_custom)
@ -422,6 +428,15 @@ function conversationInit()
} }
e.preventDefault(); e.preventDefault();
}); });
// View Send Log
jQuery(".thread-send-log-trigger").click(function(e){
var thread_id = $(this).parents('.thread:first').attr('data-thread_id');
if (!thread_id) {
return;
}
e.preventDefault();
});
}); });
} }
@ -707,6 +722,9 @@ function showModal(a, onshow)
title = a.text(); title = a.text();
} }
var remote = a.attr('data-remote'); var remote = a.attr('data-remote');
if (!remote) {
remote = a.attr('href');
}
var body = a.attr('data-modal-body'); var body = a.attr('data-modal-body');
var footer = a.attr('data-modal-footer'); var footer = a.attr('data-modal-footer');
var no_close_btn = a.attr('data-no-close-btn'); var no_close_btn = a.attr('data-no-close-btn');

View File

@ -0,0 +1,59 @@
@if (!$customers_log && !$users_log)
<div class="alert alert-warning">{{ __("Log is empty") }}</div>
@else
@if (!empty($customers_log))
<h5>{{ __("Emails to customers") }}</h5>
<table class="table table-striped">
<tr>
<th>{{ __("Customer") }}</th>
<th>{{ __("Date") }}</th>
<th>{{ __("Status") }}</th>
</tr>
@foreach ($customers_log as $email => $logs)
@foreach ($logs as $log)
<tr>
@if ($loop->index == 0)
<td rowspan="{{ count($logs) }}">{{ $email }}</td>
@endif
<td class="small">{{ App\User::dateFormat($log->created_at) }}</td>
<td>
<span class="@if ($log->isErrorStatus())text-danger @elseif ($log->isSuccessStatus()) text-success @endif">{{ $log->getStatusName() }}</span>
@if ($log->status_message)
<div class="text-help">{{ $log->status_message }}</div>
@endif
</td>
</tr>
@endforeach
@endforeach
</table>
@endif
@if (!empty($users_log))
<h5>{{ __("Emails to users") }}</h5>
<table class="table table-striped">
<tr>
<th>{{ __("User") }}</th>
<th>{{ __("Date") }}</th>
<th>{{ __("Status") }}</th>
</tr>
@foreach ($users_log as $email => $logs)
@foreach ($logs as $log)
<tr>
@if ($loop->index == 0)
<td rowspan="{{ count($logs) }}">{{ $email }}</td>
@endif
<td class="small">{{ App\User::dateFormat($log->created_at) }}</td>
<td>
<span class="@if ($log->isErrorStatus())text-danger @elseif ($log->isSuccessStatus()) text-success @endif">{{ $log->getStatusName() }}</span>
@if ($log->status_message)
<div class="text-help">{{ $log->status_message }}</div>
@endif
</td>
</tr>
@endforeach
@endforeach
</table>
@endif
@endif

View File

@ -284,7 +284,11 @@
<li><a href="#" title="" class="thread-edit-trigger">{{ __("Edit") }} (todo)</a></li> <li><a href="#" title="" class="thread-edit-trigger">{{ __("Edit") }} (todo)</a></li>
<li><a href="javascript:alert('todo: implement hiding threads');void(0);" title="" class="thread-hide-trigger">{{ __("Hide") }} (todo)</a></li> <li><a href="javascript:alert('todo: implement hiding threads');void(0);" title="" class="thread-hide-trigger">{{ __("Hide") }} (todo)</a></li>
<li><a href="javascript:alert('todo: implement new conversation from thread');void(0);" title="{{ __("Start a conversation with this thread") }}" class="new-conv">{{ __("New Conversation") }}</a></li> <li><a href="javascript:alert('todo: implement new conversation from thread');void(0);" title="{{ __("Start a conversation with this thread") }}" class="new-conv">{{ __("New Conversation") }}</a></li>
<li><a href="#" title="{{ __("Show original email") }}" class="thread-orig-trigger">{{ __("Show Original") }} (todo)</a></li> @if (Auth::user()->isAdmin())
<li><a href="#" title="{{ __("Show original email headers") }}" class="thread-orig-trigger">{{ __("Show Headers") }} (todo)</a></li>
<li><a href="{{ route('conversations.ajax_html', ['action' =>
'send_log']) }}?thread_id={{ $thread->id }}" title="{{ __("View email sending log") }}" class="thread-send-log-trigger" data-trigger="modal">{{ __("View Send Log") }}</a></li>
@endif
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -43,6 +43,7 @@ Route::post('/conversation/ajax', ['uses' => 'ConversationsController@ajax', 'la
Route::post('/conversation/upload', ['uses' => 'ConversationsController@upload', 'laroute' => true])->name('conversations.upload'); Route::post('/conversation/upload', ['uses' => 'ConversationsController@upload', 'laroute' => true])->name('conversations.upload');
Route::get('/mailbox/{mailbox_id}/new-ticket', 'ConversationsController@create')->name('conversations.create'); Route::get('/mailbox/{mailbox_id}/new-ticket', 'ConversationsController@create')->name('conversations.create');
Route::get('/conversation/draft/{id}', 'ConversationsController@draft')->name('conversations.draft'); Route::get('/conversation/draft/{id}', 'ConversationsController@draft')->name('conversations.draft');
Route::get('/conversation/ajax_html/{action}', ['uses' => 'ConversationsController@ajaxHtml', 'laroute' => true])->name('conversations.ajax_html');
// Mailboxes // Mailboxes
Route::get('/settings/mailboxes', 'MailboxesController@mailboxes')->name('mailboxes'); Route::get('/settings/mailboxes', 'MailboxesController@mailboxes')->name('mailboxes');