middleware('auth');
}
/**
* View conversation.
*/
public function view(Request $request, $id)
{
$conversation = Conversation::findOrFail($id);
$this->authorize('viewCached', $conversation);
$mailbox = $conversation->mailbox;
$customer = $conversation->customer_cached;
$user = auth()->user();
// To let other parts of the app easily access.
\Helper::setGlobalEntity('conversation', $conversation);
\Helper::setGlobalEntity('mailbox', $mailbox);
if ($user->isAdmin()) {
$mailbox->fetchUserSettings($user->id);
}
// Mark notifications as read
if (!empty($request->mark_as_read)) {
$mark_read_result = $user->unreadNotifications()->where('id', $request->mark_as_read)->update(['read_at' => now()]);
$user->clearWebsiteNotificationsCache();
} else {
$mark_read_result = $user->unreadNotifications()->where('data', 'like', '%"conversation_id":'.$conversation->id.'%')->update(['read_at' => now()]);
}
if ($mark_read_result) {
$user->clearWebsiteNotificationsCache();
}
// Detect folder and redirect if needed
$folder = null;
if (Conversation::getFolderParam()) {
$folder = $conversation->mailbox->folders()->where('folders.id', Conversation::getFolderParam())->first();
// Pass some params when redirecting.
$params = [];
if (!empty($request->show_draft)) {
$params['show_draft'] = $request->show_draft;
}
if ($folder) {
// Check if conversation can be located in the passed folder_id
if (!$conversation->isInFolderAllowed($folder)) {
// Without reflash green flash will not be displayed on assignee change
\Session::reflash();
//$request->session()->reflash();
return redirect()->away($conversation->url($conversation->folder_id, null, $params));
}
// If conversation assigned to user, select Mine folder instead of Assigned
if ($folder->type == Folder::TYPE_ASSIGNED && $conversation->user_id == $user->id) {
$folder = $conversation->mailbox->folders()
->where('type', Folder::TYPE_MINE)
->where('user_id', $user->id)
->first();
\Session::reflash();
return redirect()->away($conversation->url($folder->id, null, $params));
}
}
}
// Add folder if empty
if (!$folder) {
if ($conversation->user_id == $user->id) {
$folder = $conversation->mailbox->folders()
->where('type', Folder::TYPE_MINE)
->where('user_id', $user->id)
->first();
} else {
$folder = $conversation->folder;
}
\Session::reflash();
return redirect()->away($conversation->url($folder->id));
}
//$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
$after_send = $user->mailboxSettings($conversation->mailbox_id)->after_send;
// Detect customers and emails to which user can reply
$to_customers = [];
// Add all customer emails
$customer_emails = [];
$distinct_emails = [];
// Add emails of customers from whom there were replies in the conversation
$prev_customers_emails = [];
if ($conversation->customer_email) {
$prev_customers_emails = Thread::select('from', 'customer_id')
->where('conversation_id', $id)
->where('type', Thread::TYPE_CUSTOMER)
->where('from', '<>', $conversation->customer_email)
->groupBy(['from', 'customer_id'])
->get();
}
foreach ($prev_customers_emails as $prev_customer) {
if (!in_array($prev_customer->from, $distinct_emails) && $prev_customer->customer && $prev_customer->from) {
$to_customers[] = [
'customer' => $prev_customer->customer,
'email' => $prev_customer->from,
];
$distinct_emails[] = $prev_customer->from;
}
}
// Add customer email(s) if there more than one or if there are other emails in threads.
if ($customer) {
$customer_emails = $customer->emails;
}
// This is tricky case - when customer_email is different from the
// currently selected customer.
// 1. Email has been received from a customer.
// 2. Customer has been changed.
// 3. Reply has been sent to the original customer email.
if ($conversation->customer_email
&& count($customer_emails)
&& !in_array($conversation->customer_email, $customer_emails->pluck('email')->toArray())
) {
$extra_customer_added = false;
foreach ($to_customers as $to_customer) {
if ($to_customer['email'] == $conversation->customer_email) {
$extra_customer_added = true;
break;
}
}
if (!$extra_customer_added) {
// Get customer by email.
$extra_customer = Customer::getByEmail($conversation->customer_email);
if ($extra_customer) {
$to_customers[] = [
'customer' => $extra_customer,
'email' => $conversation->customer_email,
];
}
}
}
if (count($customer_emails) > 1 || count($to_customers)) {
foreach ($customer_emails as $customer_email) {
$to_customers[] = [
'customer' => $customer,
'email' => $customer_email->email,
];
$distinct_emails[] = $customer_email->email;
}
}
// Exclude mailbox emails from $to_customers.
$mailbox_emails = $mailbox->getEmails();
foreach ($to_customers as $key => $to_customer) {
if (in_array($to_customer['email'], $mailbox_emails)) {
unset($to_customers[$key]);
}
}
$threads = $conversation->threads()->orderBy('created_at', 'desc')->get();
// Get To for new conversation.
$new_conv_to = [];
if (empty($threads[0]) || empty($threads[0]->to)) {
// Before new conversation To field was stored in $conversation->customer_email.
$emails = Conversation::sanitizeEmails($conversation->customer_email);
// Get customers info for emails.
if (count($emails)) {
$new_conv_to = Customer::emailsToCustomers($emails);
}
} else {
$new_conv_to = Customer::emailsToCustomers($threads[0]->getToArray());
}
if (empty($customer) && count($new_conv_to) == 1) {
$customer = Customer::getByEmail(array_key_first($new_conv_to));
}
// Previous conversations
$prev_conversations = [];
if ($customer) {
$prev_conversations = $mailbox->conversations()
->where('customer_id', $customer->id)
->where('id', '<>', $conversation->id)
->where('status', '!=', Conversation::STATUS_SPAM)
->where('state', Conversation::STATE_PUBLISHED)
//->limit(self::PREV_CONVERSATIONS_LIMIT)
->orderBy('created_at', 'desc')
->paginate(self::PREV_CONVERSATIONS_LIMIT);
}
$template = 'conversations/view';
if ($conversation->state == Conversation::STATE_DRAFT) {
$template = 'conversations/create';
}
// CC.
$exclude_array = $conversation->getExcludeArray($mailbox);
$cc = $conversation->getCcArray($exclude_array);
// If last reply came from customer who was mentioned in CC before,
// we need to add this customer as CC.
// https://github.com/freescout-helpdesk/freescout/issues/3613
foreach ($threads as $thread) {
if ($thread->isUserMessage() && !$thread->isDraft()) {
break;
}
if ($thread->isCustomerMessage()) {
if ($thread->customer_id != $conversation->customer_id) {
$cc[] = $thread->from;
}
break;
}
}
// Get data for creating a phone conversation.
$name = [];
$phone = '';
$to_email = [];
if ($customer) {
if ($customer->getFullName()) {
$name = [$customer->id => $customer->getFullName()];
}
$last_phone = array_last($customer->getPhones());
if (!empty($last_phone)) {
$phone = $last_phone['value'];
}
if ($conversation->customer_email) {
$customer_email = $conversation->customer_email;
} else {
$customer_email = $customer->getMainEmail();
}
if ($customer_email) {
$to_email = [$customer_email];
}
}
// Notify other users that current user is viewing conversation.
// Eventually notification data will be saved in polycast_events table and processes
// in JS in users browsers.
// $notification = new \App\Notifications\UserViewingConversationNotification(
// $conversation, $user, false
// );
// This broadcasts to specific users.
// \Notification::send($mailbox->usersHavingAccess(), $notification);
// Notification is sent to all via public channel: conview
// If we send notification to each user, applications having thouthans of users
// will be overloaded.
// // https://laravel.com/docs/5.5/broadcasting#broadcasting-events
\App\Events\RealtimeConvView::dispatchSelf($conversation->id, $user, false);
// Get viewers.
$viewers = [];
$conv_view = \Cache::get('conv_view');
if ($conv_view && !empty($conv_view[$conversation->id])) {
$viewing_users = User::whereIn('id', array_keys($conv_view[$conversation->id]))->get();
foreach ($viewing_users as $viewer) {
if (isset($conv_view[$conversation->id][$viewer->id]['r']) && $viewer->id != $user->id) {
$viewers[] = [
'user' => $viewer,
'replying' => (int)$conv_view[$conversation->id][$viewer->id]['r']
];
}
}
// Show replying first.
usort($viewers, function($a, $b) {
return $b['replying'] <=> $a['replying'];
});
}
$is_following = $conversation->isUserFollowing($user->id);
\Eventy::action('conversation.view.start', $conversation, $request);
// Mailbox aliases.
$from_aliases = $conversation->mailbox->getAliases(true, true);
$from_alias = '';
if (count($from_aliases) == 1) {
$from_aliases = [];
}
if ($conversation->isDraft() && !empty($threads[0])) {
$from_alias = $threads[0]->from ?? '';
}
if (count($from_aliases) && !$from_alias) {
// Preset the last alias used.
$check_initial_thread = true;
foreach ($threads as $thread) {
if ($thread->isUserMessage() && !$thread->isDraft()) {
$check_initial_thread = false;
if ($thread->from) {
$from_alias = $thread->from;
}
break;
}
}
// Maybe the first email has been sent to some mailbox alias.
if (!$from_alias && $check_initial_thread) {
$initial_thread = $threads->last();
if ($initial_thread && $initial_thread->isCustomerMessage()) {
$initial_recipients = $initial_thread->getToArray();
$initial_recipients = array_merge($initial_recipients, $initial_thread->getCcArray());
foreach ($initial_recipients as $initial_recipient) {
foreach ($from_aliases as $from_alias_email => $dummy) {
if ($initial_recipient == $from_alias_email) {
$from_alias = $from_alias_email;
break 2;
}
}
}
}
}
}
return view($template, [
'conversation' => $conversation,
'mailbox' => $conversation->mailbox,
'customer' => $customer,
'threads' => \Eventy::filter('conversation.view.threads', $threads),
'folder' => $folder,
'folders' => $conversation->mailbox->getAssesibleFolders(),
'after_send' => $after_send,
'to' => $new_conv_to,
'to_customers' => $to_customers,
'prev_conversations' => $prev_conversations,
'cc' => $cc,
'bcc' => [], //$conversation->getBccArray($exclude_array),
// Data for creating a phone conversation.
'name' => $name,
'phone' => $phone,
'to_email' => $to_email,
'viewers' => $viewers,
'is_following' => $is_following,
'from_aliases' => $from_aliases,
'from_alias' => $from_alias,
]);
}
/**
* New conversation.
*/
public function create(Request $request, $mailbox_id)
{
$mailbox = Mailbox::findOrFail($mailbox_id);
$this->authorize('view', $mailbox);
$subject = trim($request->get('subject') ?? '');
$conversation = new Conversation();
$conversation->body = '';
$conversation->mailbox = $mailbox;
$folder = $mailbox->folders()->where('type', Folder::TYPE_DRAFTS)->first();
// todo: use $user->mailboxSettings()
$after_send = $mailbox->getUserSettings(auth()->user()->id)->after_send;
// Create conversation from thread
$thread = null;
if (!empty($request->from_thread_id)) {
$orig_thread = Thread::find($request->from_thread_id);
if ($orig_thread) {
$subject = $orig_thread->conversation->subject;
$subject = preg_replace('/^Fwd:/i', 'Re: ', $subject);
$thread = new \App\Thread();
$thread->body = $orig_thread->body;
// If this is a forwarded message, try to fetch From
preg_match_all("/From:[^<\n]+<([^<\n]+)>/m", html_entity_decode(strip_tags($thread->body)), $m);
if (!empty($m[1])) {
foreach ($m[1] as $value) {
if (\MailHelper::validateEmail($value)) {
$thread->to = json_encode([$value]);
break;
}
}
}
}
}
$to = [];
// Prefill some values.
$prefill_to = \App\Email::sanitizeEmail($request->get('to'));
if ($prefill_to) {
$to = [$prefill_to => $prefill_to];
}
$conversation->subject = $subject;
return view('conversations/create', [
'conversation' => $conversation,
'thread' => $thread,
'mailbox' => $mailbox,
'folder' => $folder,
'folders' => $mailbox->getAssesibleFolders(),
'after_send' => $after_send,
'to' => $to,
'from_aliases' => $mailbox->getAliases(true, true),
]);
}
/**
* Clone conversation.
*/
public function cloneConversation(Request $request, $mailbox_id, $from_thread_id)
{
$mailbox = Mailbox::findOrFail($mailbox_id);
$this->authorize('view', $mailbox);
if (!empty($from_thread_id)) {
$orig_thread = Thread::find($from_thread_id);
if ($orig_thread) {
$orign_conv = $orig_thread->conversation;
$this->authorize('view', $orign_conv);
// $thread = $orig_thread->replicate();
// $thread->id = '';
// $thread->message_id .= ".clone".crc32(mktime());
// $thread->status = Thread::STATUS_ACTIVE;
// $thread->conversation_id = $conversation->id;
// $thread->save();
$now = date('Y-m-d H:i:s');
$conversation = new Conversation();
$conversation->type = $orign_conv->type;
$conversation->subject = $orign_conv->subject;
$conversation->mailbox_id = $orign_conv->mailbox_id;
$conversation->preview = '';
// Preset source_via here to avoid error in PostgreSQL.
$conversation->source_via = $orign_conv->source_via;
$conversation->source_type = $orign_conv->source_type;
$conversation->customer_id = $orign_conv->customer_id;
$conversation->customer_email = $orign_conv->customer->getMainEmail();
$conversation->status = Conversation::STATUS_ACTIVE;
$conversation->state = Conversation::STATE_PUBLISHED;
$conversation->cc = $orig_thread->cc;
$conversation->bcc = $orig_thread->bcc;
// Set assignee
$conversation->user_id = $orign_conv->user_id;
$conversation->updateFolder();
$conversation->save();
$thread = Thread::createExtended([
'conversation_id' => $orig_thread->conversation_id,
'user_id' => $orig_thread->user_id,
'type' => $orig_thread->type,
'status' => $conversation->status,
'state' => $conversation->state,
'body' => $orig_thread->body,
'headers' => $orig_thread->headers,
'from' => $orig_thread->from,
'to' => $orig_thread->to,
'cc' => $orig_thread->cc,
'bcc' => $orig_thread->bcc,
//'attachments' => $attachments,
'has_attachments' => $orig_thread->has_attachments,
'message_id' => "clone".crc32(microtime()).'-'.$orig_thread->message_id,
'source_via' => $orig_thread->source_via,
'source_type' => $orig_thread->source_type,
'customer_id' => $orig_thread->customer_id,
'created_by_customer_id' => $orig_thread->created_by_customer_id,
],
$conversation
);
// Clone attachments.
$attachments = Attachment::where('thread_id', $orig_thread->id)->get();
foreach ($attachments as $attachment) {
$attachment->duplicate($thread->id);
}
return redirect()->away($conversation->url());
} else {
return redirect()->away($mailbox->url());
}
} else {
return redirect()->away($mailbox->url());
}
}
/**
* Conversation draft.
*/
// public function draft($id)
// {
// $conversation = Conversation::findOrFail($id);
// $this->authorize('view', $conversation);
// return view('conversations/create', [
// 'conversation' => $conversation,
// 'mailbox' => $conversation->mailbox,
// 'folder' => $conversation->folder,
// 'folders' => $conversation->mailbox->getAssesibleFolders(),
// ]);
// }
/**
* Conversations ajax controller.
*/
public function ajax(Request $request)
{
$response = [
'status' => 'error',
'msg' => '', // this is error message
];
$user = auth()->user();
switch ($request->action) {
// Change conversation user
case 'conversation_change_user':
$conversation = Conversation::find($request->conversation_id);
$new_user_id = (int) $request->user_id;
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && $conversation->user_id == $new_user_id) {
$response['msg'] = __('Assignee already set');
}
if (!$response['msg'] && !$user->can('update', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg'] && (int) $new_user_id != -1 && !$conversation->mailbox->userHasAccess($new_user_id)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
// Determine redirect
// Must be done before updating current conversation's status or assignee.
$redirect_same_page = false;
if ($new_user_id == $user->id || $request->x_embed == 1) {
// If user assigned conversation to himself, stay on the current page
$response['redirect_url'] = $conversation->url();
$redirect_same_page = true;
} else {
$response['redirect_url'] = $this->getRedirectUrl($request, $conversation, $user);
}
$conversation->changeUser($new_user_id, $user);
$response['status'] = 'success';
// Flash
$flash_message = __('Assignee updated');
if (!$redirect_same_page || $response['redirect_url'] != $conversation->url()) {
$flash_message .= ' '.__('View').'';
}
\Session::flash('flash_success_floating', $flash_message);
$response['msg'] = __('Assignee updated');
}
break;
// Change conversation status
case 'conversation_change_status':
$conversation = Conversation::find($request->conversation_id);
if ($request->status == 'not_spam') {
// Find previous status in threads
$new_status = $conversation
->threads()
->orderBy('created_at', 'desc')
->where('status', '!=', Thread::STATUS_SPAM)
->where('type', Thread::TYPE_LINEITEM)
->where('action_type', Thread::ACTION_TYPE_STATUS_CHANGED)
->value('status');
if (!$new_status) {
$new_status = Thread::STATUS_ACTIVE;
}
} else {
$new_status = (int) $request->status;
}
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && $conversation->status == $new_status) {
$response['msg'] = __('Status already set');
}
if (!$response['msg'] && !$user->can('update', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg'] && !in_array((int) $new_status, array_keys(Conversation::$statuses))) {
$response['msg'] = __('Incorrect status');
}
if (!$response['msg']) {
// Determine redirect
// Must be done before updating current conversation's status or assignee.
$redirect_same_page = false;
if ($request->status == 'not_spam' || $request->x_embed == 1) {
// Stay on the current page
$response['redirect_url'] = $conversation->url();
$redirect_same_page = true;
} else {
$response['redirect_url'] = $this->getRedirectUrl($request, $conversation, $user);
}
$conversation->changeStatus($new_status, $user);
$response['status'] = 'success';
// Flash
$flash_message = __('Status updated');
if (!$redirect_same_page || $response['redirect_url'] != $conversation->url()) {
$flash_message .= ' '.__('View').'';
}
\Session::flash('flash_success_floating', $flash_message);
$response['msg'] = __('Status updated');
}
break;
// Send reply, new conversation, add note or forward
case 'send_reply':
$mailbox = Mailbox::findOrFail($request->mailbox_id);
if (!$response['msg'] && !$user->can('view', $mailbox)) {
$response['msg'] = __('Not enough permissions');
}
$conversation = null;
if (!$response['msg'] && !empty($request->conversation_id)) {
$conversation = Conversation::find($request->conversation_id);
if ($conversation && !$user->can('view', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
}
$new = false;
if (empty($request->conversation_id)) {
$new = true;
}
$is_note = false;
if (!empty($request->is_note)) {
$is_note = true;
}
// Conversation type.
$type = Conversation::TYPE_EMAIL;
if (!empty($request->type)) {
$type = (int)$request->type;
} elseif ($conversation) {
$type = $conversation->type;
}
$is_phone = false;
if ($type == Conversation::TYPE_PHONE) {
$is_phone = true;
}
$is_create = false;
if (!empty($request->is_create)) {
//if ($new || ($from_draft && $conversation->threads_count == 1)) {
$is_create = $request->is_create;
}
$is_forward = false;
if (!empty($request->subtype) && (int)$request->subtype == Thread::SUBTYPE_FORWARD) {
$is_forward = true;
}
$is_multiple = false;
if (!empty($request->multiple_conversations)) {
$is_multiple = true;
}
// If reply is being created from draft, there is already thread created
$thread = null;
$from_draft = false;
if ((!$is_note || $is_phone) && !$response['msg'] && !empty($request->thread_id)) {
$thread = Thread::find($request->thread_id);
if ($thread && (!$conversation || $thread->conversation_id != $conversation->id)) {
$response['msg'] = __('Incorrect thread');
} else {
$from_draft = true;
}
}
if (!$response['msg']) {
if ($thread && $from_draft && $thread->state == Thread::STATE_PUBLISHED) {
$response['msg'] = __('Message has been already sent. Please discard this draft.');
}
}
// Validate form
if (!$response['msg']) {
if ($new) {
if ($type == Conversation::TYPE_EMAIL) {
$validator = Validator::make($request->all(), [
'to' => 'required|array',
'subject' => 'required|string|max:998',
'body' => 'required|string',
'cc' => 'nullable|array',
'bcc' => 'nullable|array',
]);
} else {
// Phone conversation.
$validator = Validator::make($request->all(), [
'name' => 'required|string',
'subject' => 'required|string|max:998',
'body' => 'required|string',
'phone' => 'nullable|string',
'to_email' => 'nullable|string',
]);
}
} else {
$validator = Validator::make($request->all(), [
'body' => 'required|string',
'cc' => 'nullable|array',
'bcc' => 'nullable|array',
]);
}
if ($validator->fails()) {
foreach ($validator->errors()->getMessages()as $errors) {
foreach ($errors as $field => $message) {
$response['msg'] .= $message.' ';
}
}
}
}
$body = $request->body;
// Replace base64 images with attachment URLs in case text
// was copy and pasted into the editor.
// https://github.com/freescout-helpdesk/freescout/issues/3057
$body = Thread::replaceBase64ImagesWithAttachments($body);
// List of emails.
$to_array = [];
if ($is_forward) {
$to_array = Conversation::sanitizeEmails($request->to_email);
} else {
$to_array = Conversation::sanitizeEmails($request->to);
}
// Check To
if (!$response['msg'] && $new && !$is_phone) {
if (!$to_array) {
$response['msg'] .= __('Incorrect recipients');
}
}
// Check max. message size.
if (!$response['msg']) {
$max_message_size = (int)config('app.max_message_size');
if ($max_message_size) {
// Todo: take into account conversation history.
$message_size = mb_strlen($body, '8bit');
// Calculate attachments size.
$attachments_ids = array_merge($request->attachments ?? [], $request->embeds ?? []);
if (count($attachments_ids)) {
$attachments_to_check = Attachment::select('size')->whereIn('id', $attachments_ids)->get();
foreach ($attachments_to_check as $attachment) {
$message_size += (int)$attachment->size;
}
}
if ($message_size > $max_message_size*1024*1024) {
$response['msg'] = __('Message is too large — :info. Please shorten your message or remove some attachments.', ['info' => __('Max. Message Size').': '.$max_message_size.' MB']);
}
}
}
if (!$response['msg']) {
// Get attachments info
// Delete removed attachments.
$attachments_info = $this->processReplyAttachments($request);
// Determine redirect.
// Must be done before updating current conversation's status or assignee.
// Redirect URL for new no saved yet conversation is determined below.
if (!$new) {
$response['redirect_url'] = $this->getRedirectUrl($request, $conversation, $user);
}
// Conversation
$now = date('Y-m-d H:i:s');
$status_changed = false;
$user_changed = false;
if ($new) {
// New conversation
$conversation = new Conversation();
$conversation->type = $type;
$conversation->subject = $request->subject;
$conversation->setPreview($body);
$conversation->mailbox_id = $request->mailbox_id;
$conversation->created_by_user_id = auth()->user()->id;
$conversation->source_via = Conversation::PERSON_USER;
$conversation->source_type = Conversation::SOURCE_TYPE_WEB;
} else {
// Reply or note
if ((int) $request->status != (int) $conversation->status) {
$status_changed = true;
}
if (!empty($request->subject)) {
$conversation->subject = $request->subject;
}
// When switching from regular message to phone and message sent
// without saving a draft type need to be saved here.
// Or vise versa.
if (($conversation->type == Conversation::TYPE_EMAIL && $type == Conversation::TYPE_PHONE)
|| ($conversation->type == Conversation::TYPE_PHONE && $type == Conversation::TYPE_EMAIL)
) {
$conversation->type = $type;
}
// Allow to convert phone conversations into email conversations.
if ($conversation->isPhone() && !$is_note && $conversation->customer
&& $customer_email = $conversation->customer->getMainEmail()
) {
$conversation->type = Conversation::TYPE_EMAIL;
$conversation->customer_email = $customer_email;
$is_phone = false;
}
}
if ($attachments_info['has_attachments']) {
$conversation->has_attachments = true;
}
// Customer can be empty in existing conversation if this is a draft.
$customer_email = '';
$customer = null;
if ($is_phone && $is_create) {
// Phone.
$phone_customer_data = $this->processPhoneCustomer($request);
$customer_email = $phone_customer_data['customer_email'];
$customer = $phone_customer_data['customer'];
if (!$conversation->customer_id) {
$conversation->customer_id = $customer->id;
}
} else {
// Email or reply to a phone conversation.
if (!empty($to_array)) {
$customer_email = $to_array[0];
} elseif (!$conversation->customer_email
&& ($conversation->isEmail() || $conversation->isPhone())
&& $conversation->customer_id
&& $conversation->customer
) {
// When replying to a phone conversation, we need to
// set 'customer_email' for the conversation.
$customer_email = $conversation->customer->getMainEmail();
}
if (!$conversation->customer_id) {
$customer = Customer::create($customer_email);
$conversation->customer_id = $customer->id;
} else {
$customer = $conversation->customer;
}
}
if ($customer_email && !$is_note && !$is_forward) {
$conversation->customer_email = $customer_email;
}
$prev_status = $conversation->status;
$conversation->status = $request->status;
if (($prev_status != $conversation->status || $is_create)
&& $conversation->status == Conversation::STATUS_CLOSED
) {
$conversation->closed_by_user_id = $user->id;
$conversation->closed_at = date('Y-m-d H:i:s');
}
// We need to set state, as it may have been a draft.
$prev_state = $conversation->state;
$conversation->state = Conversation::STATE_PUBLISHED;
// Set assignee
$prev_user_id = $conversation->user_id;
if ((int) $request->user_id != -1) {
// Check if user has access to the current mailbox
if ((int) $conversation->user_id != (int) $request->user_id && $mailbox->userHasAccess($request->user_id)) {
$conversation->user_id = $request->user_id;
$user_changed = true;
}
} else {
$conversation->user_id = null;
}
// To is a single email string.
$to = '';
// List of emails.
$to_list = [];
if ($is_forward) {
if (empty($request->to_email[0])) {
$response['msg'] = __('Please specify a recipient.');
break;
}
$to = $request->to_email[0];
} else {
if (!empty($request->to)) {
// When creating a new conversation, to is a list of emails.
if (is_array($request->to)) {
$to = $request->to[0];
} else {
$to = $request->to;
}
} else {
$to = $conversation->customer_email;
}
}
if (!$is_note && !$is_forward) {
// Save extra recipients to CC
if ($is_create && !$is_multiple && count($to_array) > 1) {
$conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), $to_array));
} else {
if (!$is_multiple) {
$conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
} else {
$conversation->setCc(Conversation::sanitizeEmails($request->cc));
}
}
$conversation->setBcc($request->bcc);
$conversation->last_reply_at = $now;
$conversation->last_reply_from = Conversation::PERSON_USER;
$conversation->user_updated_at = $now;
}
if ($conversation->isPhone() && $is_note) {
$conversation->last_reply_at = $now;
$conversation->last_reply_from = Conversation::PERSON_USER;
}
$conversation->updateFolder();
if ($from_draft) {
// Increment number of replies in conversation
$conversation->threads_count++;
// We need to set preview here as when conversation is created from draft,
// ThreadObserver::created() method is not called.
$conversation->setPreview($body);
}
$conversation->save();
// Redirect URL for new not saved yet conversation must be determined here.
if ($new) {
$response['redirect_url'] = $this->getRedirectUrl($request, $conversation, $user);
}
// Fire events
\Eventy::action('conversation.send_reply_save', $conversation, $request);
if (!$new) {
if ($status_changed) {
event(new ConversationStatusChanged($conversation));
\Eventy::action('conversation.status_changed', $conversation, $user, $changed_on_reply = true, $prev_status);
}
if ($user_changed) {
event(new ConversationUserChanged($conversation, $user));
\Eventy::action('conversation.user_changed', $conversation, $user, $prev_user_id);
}
}
if ($conversation->state != $prev_state) {
\Eventy::action('conversation.state_changed', $conversation, $user, $prev_state);
}
// Create thread
if (!$thread) {
$thread = new Thread();
$thread->conversation_id = $conversation->id;
if ($is_note || $is_forward) {
$thread->type = Thread::TYPE_NOTE;
} else {
$thread->type = Thread::TYPE_MESSAGE;
}
$thread->source_via = Thread::PERSON_USER;
$thread->source_type = Thread::SOURCE_TYPE_WEB;
} else {
if ($is_forward || $is_phone) {
$thread->type = Thread::TYPE_NOTE;
} else {
$thread->type = Thread::TYPE_MESSAGE;
}
$thread->created_at = $now;
}
if ($new) {
$thread->first = true;
}
$thread->user_id = $conversation->user_id;
$thread->status = $request->status;
$thread->state = Thread::STATE_PUBLISHED;
$thread->customer_id = $customer->id;
$thread->created_by_user_id = auth()->user()->id;
$thread->edited_by_user_id = null;
$thread->edited_at = null;
$thread->body = $body;
if ($is_create && !$is_multiple && count($to_array) > 1) {
$thread->setTo($to_array);
} else {
$thread->setTo($to);
}
// We save CC and BCC as is and filter emails when sending replies
$thread->setCc($request->cc);
$thread->setBcc($request->bcc);
if ($attachments_info['has_attachments'] && !$is_forward) {
$thread->has_attachments = true;
}
if (!empty($request->saved_reply_id)) {
$thread->saved_reply_id = $request->saved_reply_id;
}
$forwarded_conversations = [];
$forwarded_threads = [];
if ($is_forward) {
// Create forwarded conversations.
foreach ($to_array as $recipient_email) {
$forwarded_conversation = $conversation->replicate();
$forwarded_conversation->type = Conversation::TYPE_EMAIL;
$forwarded_conversation->setPreview($thread->body);
$forwarded_conversation->created_by_user_id = auth()->user()->id;
$forwarded_conversation->source_via = Conversation::PERSON_USER;
$forwarded_conversation->source_type = Conversation::SOURCE_TYPE_WEB;
$forwarded_conversation->threads_count = 0; // Counter will be incremented in ThreadObserver.
$forwarded_customer = Customer::create($recipient_email);
$forwarded_conversation->customer_id = $forwarded_customer->id;
// Reload customer object, otherwise it stores previous customer.
$forwarded_conversation->load('customer');
$forwarded_conversation->customer_email = $recipient_email;
$forwarded_conversation->subject = 'Fwd: '.$forwarded_conversation->subject;
//$forwarded_conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
$forwarded_conversation->setCc(Conversation::sanitizeEmails($request->cc));
$forwarded_conversation->setBcc($request->bcc);
$forwarded_conversation->last_reply_at = $now;
$forwarded_conversation->last_reply_from = Conversation::PERSON_USER;
$forwarded_conversation->user_updated_at = $now;
if ($attachments_info['has_attachments']) {
$forwarded_conversation->has_attachments = true;
}
$forwarded_conversation->updateFolder();
$forwarded_conversation->save();
$forwarded_thread = $thread->replicate();
$forwarded_conversations[] = $forwarded_conversation;
$forwarded_threads[] = $forwarded_thread;
}
// Set forwarding meta data.
// todo: store array of numbers and IDs.
$thread->subtype = Thread::SUBTYPE_FORWARD;
$thread->setMeta(Thread::META_FORWARD_CHILD_CONVERSATION_NUMBER, $forwarded_conversation->number);
$thread->setMeta(Thread::META_FORWARD_CHILD_CONVERSATION_ID, $forwarded_conversation->id);
}
// Conversation history.
if (!empty($request->conv_history)) {
if ($request->conv_history != 'global') {
if ($is_forward && !empty($forwarded_threads)) {
foreach ($forwarded_threads as $forwarded_thread) {
$forwarded_thread->setMeta(Thread::META_CONVERSATION_HISTORY, $request->conv_history);
}
} else {
$thread->setMeta(Thread::META_CONVERSATION_HISTORY, $request->conv_history);
}
}
}
// From (mailbox alias).
if (!empty($request->from_alias)) {
$thread->from = $request->from_alias;
}
\Eventy::action('thread.before_save_from_request', $thread, $request);
$thread->save();
// Save forwarded thread.
if ($is_forward) {
foreach ($forwarded_conversations as $i => $forwarded_conversation) {
$forwarded_thread = $forwarded_threads[$i];
$forwarded_thread->conversation_id = $forwarded_conversation->id;
$forwarded_thread->type = Thread::TYPE_MESSAGE;
$forwarded_thread->subtype = null;
if ($attachments_info['has_attachments']) {
$forwarded_thread->has_attachments = true;
}
$forwarded_thread->setMeta(Thread::META_FORWARD_PARENT_CONVERSATION_NUMBER, $conversation->number);
$forwarded_thread->setMeta(Thread::META_FORWARD_PARENT_CONVERSATION_ID, $conversation->id);
$forwarded_thread->setMeta(Thread::META_FORWARD_PARENT_THREAD_ID, $thread->id);
\Eventy::action('send_reply.before_save_forwarded_thread', $forwarded_thread, $request);
$forwarded_thread->save();
}
}
// If thread has been created from draft, remove the draft
// if ($request->thread_id) {
// $draft_thread = Thread::find($request->thread_id);
// if ($draft_thread) {
// $draft_thread->delete();
// }
// }
if ($from_draft) {
// Remove conversation from drafts folder if needed
$conversation->maybeRemoveFromDrafts();
}
// Update folders counters
$conversation->mailbox->updateFoldersCounters();
$response['status'] = 'success';
// Set thread_id for uploaded attachments
if ($attachments_info['attachments']) {
if ($is_forward) {
// Copy attachments for each thread.
if (count($forwarded_threads) > 1) {
$attachments = Attachment::whereIn('id', $attachments_info['attachments'])->get();
}
foreach ($forwarded_threads as $i => $forwarded_thread) {
if ($i == 0) {
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $forwarded_thread->id]);
} else {
foreach ($attachments as $attachment) {
$attachment->duplicate($forwarded_thread->id);
}
}
}
} else {
Attachment::whereIn('id', $attachments_info['attachments'])
->where('thread_id', null)
->update(['thread_id' => $thread->id]);
}
}
// Follow conversation if it's assigned to someone else.
if (!$is_create && !$new && !$is_forward && !$is_note
&& $conversation->user_id != $user->id
) {
$user->followConversation($conversation->id);
}
// When user creates a new conversation it may be saved as draft first.
if ($is_create) {
// New conversation.
event(new UserCreatedConversation($conversation, $thread));
\Eventy::action('conversation.created_by_user_can_undo', $conversation, $thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
} elseif ($is_forward) {
// Forward.
// Notifications to users not sent.
event(new UserAddedNote($conversation, $thread));
foreach ($forwarded_conversations as $i => $forwarded_conversation) {
$forwarded_thread = $forwarded_threads[$i];
// To send email with forwarded conversation.
event(new UserReplied($forwarded_conversation, $forwarded_thread));
\Eventy::action('conversation.user_forwarded_can_undo', $conversation, $thread, $forwarded_conversation, $forwarded_thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.user_forwarded', [$conversation, $thread, $forwarded_conversation, $forwarded_thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
}
} elseif ($is_note) {
// Note.
event(new UserAddedNote($conversation, $thread));
\Eventy::action('conversation.note_added', $conversation, $thread);
} else {
// Reply.
event(new UserReplied($conversation, $thread));
\Eventy::action('conversation.user_replied_can_undo', $conversation, $thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
}
// Send new conversation separately to each customer.
if ($is_create && count($to_array) > 1 && $is_multiple) {
$prev_customers_ids = [];
foreach ($to_array as $i => $customer_email) {
// Skip first email, as conversation has already been created for it.
if ($i == 0) {
continue;
}
// Get customer by email.
$customer_tmp = Customer::getByEmail($customer_email);
// Skip same customers.
if ($customer_tmp && in_array($customer_tmp->id, $prev_customers_ids)) {
continue;
}
if (!$customer_tmp) {
$customer_tmp = Customer::create($customer_email);
}
$prev_customers_ids[] = $customer_tmp->id;
// Copy conversation and thread.
$conversation_copy = $conversation->replicate();
$thread_copy = $thread->replicate();
// Save conversation.
$conversation_copy->threads_count = 0;
$conversation_copy->customer_id = $customer_tmp->id;
// Reload customer, otherwise all recipients will have the same name.
$conversation_copy->load('customer');
$conversation_copy->customer_email = $customer_email;
$conversation_copy->has_attachments = $conversation->has_attachments;
$conversation_copy->push();
$thread_copy->conversation_id = $conversation_copy->id;
$thread_copy->customer_id = $customer_tmp->id;
$thread_copy->has_attachments = $conversation->has_attachments;
$thread_copy->setTo($customer_email);
// Reload the conversation, otherwise Thread observer will be
// increasing threads_count for the first conversation.
$thread_copy->load('conversation');
$thread_copy->push();
// Copy attachments.
if (!empty($attachments_info['attachments'])) {
$attachments = Attachment::whereIn('id', $attachments_info['attachments'])->get();
foreach ($attachments as $attachment) {
$attachment->duplicate($thread_copy->id);
}
}
// Events.
// todo: allow to undo all emails
event(new UserCreatedConversation($conversation_copy, $thread_copy));
\Eventy::action('conversation.created_by_user_can_undo', $conversation_copy, $thread_copy);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.created_by_user', [$conversation_copy, $thread_copy], now()->addSeconds(Conversation::UNDO_TIMOUT));
}
}
// Compose flash message.
$show_view_link = true;
if (!empty($request->after_send) && $request->after_send == MailboxUser::AFTER_SEND_STAY) {
$show_view_link = false;
}
$flash_vars = ['%tag_start%' => '', '%tag_end%' => '', '%view_start%' => ' ', '%a_end%' => ' ', '%undo_start%' => ' '];
if ($is_phone) {
$flash_type = 'warning';
if ($show_view_link) {
$flash_text = __(':%tag_start%Conversation created:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%', $flash_vars);
} else {
$flash_text = ''.__('Conversation created').'';
}
} elseif ($is_note) {
$flash_type = 'warning';
if ($show_view_link) {
$flash_text = __(':%tag_start%Note added:%tag_end% :%view_start%View:%a_end%', $flash_vars);
} else {
$flash_text = ''.__('Note added').'';
}
} else {
$flash_type = 'success';
if ($show_view_link) {
$flash_text = __(':%tag_start%Email Sent:%tag_end% :%view_start%View:%a_end% or :%undo_start%Undo:%a_end%', $flash_vars);
} else {
$flash_text = __(':%tag_start%Email Sent:%tag_end% :%undo_start%Undo:%a_end%', $flash_vars);
}
}
\Session::flash('flash_'.$flash_type.'_floating', $flash_text);
}
break;
// Save draft (automatically or by click) of a new conversation or reply.
case 'save_draft':
$mailbox = Mailbox::findOrFail($request->mailbox_id);
if (!$response['msg'] && !$user->can('view', $mailbox)) {
$response['msg'] = __('Not enough permissions');
}
$conversation = null;
$new = true;
if (!$response['msg'] && !empty($request->conversation_id)) {
$conversation = Conversation::find($request->conversation_id);
if ($conversation && !$user->can('view', $conversation)) {
$response['msg'] = __('Not enough permissions');
} else {
$new = false;
}
}
$is_create = false;
if (!empty($request->is_create)) {
$is_create = true;
}
$thread = null;
$new_thread = true;
if (!$response['msg'] && !empty($request->thread_id)) {
$thread = Thread::find($request->thread_id);
if ($thread && (!$conversation || $thread->conversation_id != $conversation->id)) {
$response['msg'] = __('Incorrect thread');
} else {
$new_thread = false;
}
}
// Check if thread has been sent (in other window for example).
if (!$response['msg']) {
if ($thread && $thread->state == Thread::STATE_PUBLISHED) {
$response['msg'] = __('Message has been already sent. Please discard this draft.');
}
}
// To prevent creating draft after reply has been created.
if (!$response['msg'] && $conversation) {
// Check if the last thread has same content as the new one.
$last_thread = $conversation->getLastThread([Thread::TYPE_MESSAGE, Thread::TYPE_NOTE]);
if ($last_thread
&& $last_thread->created_by_user_id == $user->id
&& $last_thread->body == $request->body
) {
//\Log::error("You've already sent this message just recently.");
$response['msg'] = __("You've already sent this message just recently.");
}
}
// Validation is not needed on draft create, fields can be empty
if (!$response['msg']) {
// Get attachments info
$attachments_info = $this->processReplyAttachments($request);
// Conversation
$now = date('Y-m-d H:i:s');
if ($new) {
$conversation = new Conversation();
}
// To is a single email or array of emails.
$to = '';
if ($new || $is_create) {
// New conversation
$customer_email = '';
$customer = null;
$type = Conversation::TYPE_EMAIL;
if (!empty($request->type)) {
$type = (int)$request->type;
}
if ($type == Conversation::TYPE_PHONE) {
// Phone.
$phone_customer_data = $this->processPhoneCustomer($request);
$customer_email = $phone_customer_data['customer_email'];
$customer = $phone_customer_data['customer'];
} else {
// Email.
// Now instead of customer_email we store emails in thread->to.
$to_array = Conversation::sanitizeEmails($request->to);
if (count($to_array)) {
if (count($to_array) == 1) {
//$customer_email = array_first($to_array);
$to = array_first($to_array);
$customer = Customer::create($customer_email);
} else {
// Creating a conversation to multiple customers
// In customer_email temporary store a list of customer emails.
//$customer_email = implode(',', $to_array);
$to = $to_array;
// Keep $customer as null.
// When conversation will be sent, separate conversation
// will be created for each customer.
$customer = null;
}
}
}
$conversation->type = $type;
$conversation->state = Conversation::STATE_DRAFT;
$conversation->status = $request->status;
$conversation->subject = $request->subject;
$conversation->setPreview($request->body);
if ($attachments_info['has_attachments']) {
$conversation->has_attachments = true;
}
$conversation->mailbox_id = $request->mailbox_id;
// Customer may be empty in draft
if ($customer) {
$conversation->customer_id = $customer->id;
}
$conversation->customer_email = $customer_email;
$conversation->created_by_user_id = auth()->user()->id;
$conversation->source_via = Conversation::PERSON_USER;
$conversation->source_type = Conversation::SOURCE_TYPE_WEB;
} else {
// Reply
$customer = $conversation->customer;
}
// New draft conversation is not assigned to anybody
//$conversation->user_id = null;
if (empty($request->to) || !is_array($request->to)) {
if (!empty($request->to)) {
// New conversation.
$to = $request->to;
} elseif (!empty($request->to_email)) {
// Forwarding.
$to = $request->to_email;
} else {
$to = $conversation->customer_email;
}
}
// Conversation type.
if (!empty($request->type) && array_key_exists((int)$request->type, Conversation::$types)) {
$conversation->type = (int)$request->type;
}
// Save extra recipients to CC
if ($is_create) {
//$conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), (is_array($to) ? $to : [$to])));
$conversation->setCc($request->cc);
$conversation->setBcc($request->bcc);
}
// $conversation->last_reply_at = $now;
// $conversation->last_reply_from = Conversation::PERSON_USER;
// $conversation->user_updated_at = $now;
$conversation->updateFolder();
$conversation->save();
// Create thread
if (empty($thread)) {
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->user_id = auth()->user()->id;
//$thread->type = Thread::TYPE_MESSAGE;
if ($new) {
$thread->first = true;
}
//$thread->status = $request->status;
$thread->state = Thread::STATE_DRAFT;
$thread->source_via = Thread::PERSON_USER;
$thread->source_type = Thread::SOURCE_TYPE_WEB;
if ($customer) {
$thread->customer_id = $customer->id;
}
$thread->created_by_user_id = auth()->user()->id;
// User is forwarding a conversation.
if (!empty($request->subtype) && (int)$request->subtype) {
$thread->subtype = $request->subtype;
}
}
if ($attachments_info['has_attachments']) {
$thread->has_attachments = true;
}
// Thread type.
if ($is_create && !empty($request->is_note)) {
$thread->type = Thread::TYPE_NOTE;
} else {
$thread->type = Thread::TYPE_MESSAGE;
}
$thread->from = $request->from_alias ?? null;
$thread->body = $request->body;
$thread->setTo($to);
// We save CC and BCC as is and filter emails when sending replies
$thread->setCc($request->cc);
$thread->setBcc($request->bcc);
// Set edited info
if ($thread->created_by_user_id != $user->id) {
$thread->edited_by_user_id = $user->id;
$thread->edited_at = $now;
}
$thread->save();
$conversation->addToFolder(Folder::TYPE_DRAFTS);
$response['conversation_id'] = $conversation->id;
$response['customer_id'] = $conversation->customer_id;
$response['thread_id'] = $thread->id;
$response['number'] = $conversation->number;
$response['status'] = 'success';
// Set thread_id for uploaded attachments
if ($attachments_info['attachments']) {
Attachment::whereIn('id', $attachments_info['attachments'])
->where('thread_id', null)
->update(['thread_id' => $thread->id]);
}
// Update folder counter.
$conversation->mailbox->updateFoldersCounters(Folder::TYPE_DRAFTS);
if ($new) {
event(new UserCreatedConversationDraft($conversation, $thread));
} elseif ($new_thread) {
event(new UserCreatedThreadDraft($conversation, $thread));
}
$response['status'] = 'success';
}
// Reflash session data - otherwise on reply flash alert is not displayed
// https://stackoverflow.com/questions/37019294/laravel-ajax-call-deletes-session-flash-data
\Session::reflash();
break;
// Discard draft (from new conversation, from reply or conversation)
case 'discard_draft':
$thread = Thread::find($request->thread_id);
if (!$thread) {
// Discarding nont saved yet draft
$response['status'] = 'success';
// Discarding a new conversation being created from thread
if (!empty($request->from_thread_id)) {
$original_thread = Thread::find($request->from_thread_id);
if ($original_thread && $original_thread->conversation_id) {
// Open original conversation
$response['redirect_url'] = route('conversations.view', ['id' => $original_thread->conversation_id]);
}
}
break;
//$response['msg'] = __('Thread not found');
}
if (!$response['msg'] && !$user->can('view', $thread->conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$conversation = $thread->conversation;
if ($conversation->state == Conversation::STATE_DRAFT) {
// New conversation draft being discarded
$folder_id = $conversation->getCurrentFolder();
$response['redirect_url'] = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
$mailbox = $conversation->mailbox;
$conversation->removeFromFolder(Folder::TYPE_DRAFTS);
$conversation->removeFromFolder(Folder::TYPE_STARRED, $user->id);
$mailbox->updateFoldersCounters(Folder::TYPE_DRAFTS);
$conversation->deleteThreads();
$conversation->delete();
// Draft may be present in Starred folder.
Conversation::clearStarredByUserCache($user->id, $mailbox->id);
$mailbox->updateFoldersCounters(Folder::TYPE_STARRED);
$flash_message = __('Deleted draft');
\Session::flash('flash_success_floating', $flash_message);
} else {
// https://github.com/freescout-helpdesk/freescout/issues/2873
if ($thread->state == Thread::STATE_DRAFT) {
// Just remove the thread, no need to reload the page
$thread->deleteThread();
// Remove conversation from drafts folder if needed
$removed_from_folder = $conversation->maybeRemoveFromDrafts();
if ($removed_from_folder) {
$conversation->mailbox->updateFoldersCounters(Folder::TYPE_DRAFTS);
}
}
}
$response['status'] = 'success';
}
break;
// Save draft (automatically or by click)
case 'load_draft':
$thread = Thread::find($request->thread_id);
if (!$thread) {
$response['msg'] = __('Thread not found');
} elseif ($thread->state != Thread::STATE_DRAFT) {
$response['msg'] = __('Thread is not in a draft state');
} else {
if (!$user->can('view', $thread->conversation)) {
$response['msg'] = __('Not enough permissions');
}
}
if (!$response['msg']) {
// Build attachments list.
$attachments = [];
foreach ($thread->attachments as $attachment) {
$attachments[] = [
'id' => $attachment->id,
'name' => $attachment->file_name,
'size' => $attachment->size,
'url' => $attachment->url(),
];
}
$response['data'] = [
'thread_id' => $thread->id,
'from_alias' => $thread->from,
'to' => $thread->getToFirst(),
'cc' => $thread->getCcArray(),
'bcc' => $thread->getBccArray(),
'body' => $thread->body,
'is_forward' => (int)$thread->isForward(),
'attachments' => $attachments,
];
$response['status'] = 'success';
}
break;
// Load attachments from all threads in conversation
// when forwarding or creating a new conversation.
case 'load_attachments':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
} else {
if (!$user->can('view', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
}
if (!$response['msg']) {
// Build attachments list.
$attachments = [];
if ($conversation->has_attachments) {
foreach ($conversation->threads as $thread) {
if ($thread->has_attachments) {
foreach ($thread->attachments as $attachment) {
if ($request->is_forwarding == 'true') {
$attachment_copy = $attachment->duplicate($thread->id);
} else {
$attachment_copy = $attachment;
}
$attachments[] = [
'id' => $attachment_copy->id,
'name' => $attachment_copy->file_name,
'size' => $attachment_copy->size,
'url' => $attachment_copy->url(),
];
}
}
}
}
$response['data'] = [
'attachments' => $attachments,
];
$response['status'] = 'success';
}
break;
// Save default redirect
case 'save_after_send':
$mailbox = Mailbox::find($request->mailbox_id);
if (!$mailbox) {
$response['msg'] .= __('Mailbox not found');
} elseif (!$mailbox->userHasAccess($user->id)) {
$response['msg'] .= __('Action not authorized');
}
if (!$response['msg']) {
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $request->mailbox_id)->first();
if (!$mailbox_user) {
// Admin may not be connected to the mailbox yet
$user->mailboxes()->attach($request->mailbox_id);
// $mailbox_user = new MailboxUser();
// $mailbox_user->mailbox_id = $mailbox->id;
// $mailbox_user->user_id = $user->id;
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $request->mailbox_id)->first();
}
$mailbox_user->settings->after_send = $request->value;
$mailbox_user->settings->save();
$response['status'] = 'success';
}
break;
// Conversations navigation
case 'conversations_pagination':
if (!empty($request->filter)) {
$response = $this->ajaxConversationsFilter($request, $response, $user);
} else {
$response = $this->ajaxConversationsPagination($request, $response, $user);
}
break;
// Change conversation customer
case 'conversation_change_customer':
$conversation = Conversation::find($request->conversation_id);
$customer_email = $request->customer_email;
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('update', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
$response['msg'] = __('Not enough permissions');
}
$conversation->changeCustomer($customer_email, null, $user);
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Customer changed'));
break;
// Star/unstar conversation
case 'star_conversation':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
} elseif (!$user->can('view', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
if ($request->sub_action == 'star') {
$conversation->star($user);
} else {
$conversation->unstar($user);
}
$response['status'] = 'success';
}
break;
// Delete conversation (move to DELETED folder)
case 'delete_conversation':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
} elseif (!$user->can('delete', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$folder_id = $conversation->getCurrentFolder();
$conversation->deleteToFolder($user);
$response['redirect_url'] = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversation deleted'));
}
break;
// Delete conversation forever
case 'delete_conversation_forever':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
} elseif (!$user->can('delete', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$folder_id = $conversation->getCurrentFolder();
$mailbox = $conversation->mailbox;
$conversation->deleteForever();
// Recalculate only old and new folders
$mailbox->updateFoldersCounters();
$response['redirect_url'] = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversation deleted'));
}
break;
// Restore conversation
case 'restore_conversation':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
} elseif (!$user->can('delete', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$folder_id = $conversation->folder_id;
$prev_state = $conversation->state;
$conversation->state = Conversation::STATE_PUBLISHED;
$conversation->user_updated_at = date('Y-m-d H:i:s');
$conversation->updateFolder();
$conversation->save();
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->user_id = $conversation->user_id;
$thread->type = Thread::TYPE_LINEITEM;
$thread->state = Thread::STATE_PUBLISHED;
$thread->status = Thread::STATUS_NOCHANGE;
$thread->action_type = Thread::ACTION_TYPE_RESTORE_TICKET;
$thread->source_via = Thread::PERSON_USER;
// todo: this need to be changed for API
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $conversation->customer_id;
$thread->created_by_user_id = $user->id;
$thread->save();
// Recalculate only old and new folders
$conversation->mailbox->updateFoldersCounters();
if ($prev_state != $conversation->state) {
\Eventy::action('conversation.state_changed', $conversation, $user, $prev_state);
}
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversation restored'));
}
break;
// Load data to edit thread.
case 'load_edit_thread':
$thread = Thread::find($request->thread_id);
if (!$thread) {
$response['msg'] = __('Thread not found');
} elseif (!$user->can('edit', $thread)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$data = [
'thread' => $thread
];
$response['html'] = \View::make('conversations/partials/edit_thread')->with($data)->render();
$response['status'] = 'success';
}
break;
// Load data to edit thread.
case 'save_edit_thread':
$thread = Thread::find($request->thread_id);
if (!$thread) {
$response['msg'] = __('Conversation not found');
} elseif (!$user->can('edit', $thread)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
if (!$thread->body_original) {
$thread->body_original = $thread->body;
}
$thread->body = $request->body;
$thread->edited_by_user_id = $user->id;
$thread->edited_at = date('Y-m-d H:i:s');
$response['body'] = $thread->getCleanBody();
if (strip_tags($response['body'])) {
// Update the preview for the conversation if needed.
$last_thread = $thread->conversation->getLastThread([Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE, Thread::TYPE_NOTE]);
if ($last_thread && $last_thread->id == $thread->id) {
$thread->conversation->setPreview($thread->body);
$thread->conversation->save();
}
$thread->save();
$response['status'] = 'success';
} else {
$response['msg'] = __('Message cannot be empty');
}
}
break;
// Delete thread (note).
case 'delete_thread':
$thread = Thread::find($request->thread_id);
if (!$thread || !$thread->isNote()) {
$response['msg'] = __('Thread not found');
} elseif (!$user->can('delete', $thread)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$thread->deleteThread();
$response['status'] = 'success';
}
break;
// Change conversations user
case 'bulk_conversation_change_user':
$conversations = Conversation::findMany($request->conversation_id);
$new_user_id = (int) $request->user_id;
if (!$response['msg']) {
foreach ($conversations as $conversation) {
if (!$user->can('update', $conversation)) {
continue;
}
if ((int) $new_user_id != -1 && !$conversation->mailbox->userHasAccess($new_user_id)) {
continue;
}
$conversation->changeUser($new_user_id, $user);
}
$response['status'] = 'success';
// Flash
$flash_message = __('Assignee updated');
\Session::flash('flash_success_floating', $flash_message);
$response['msg'] = __('Assignee updated');
}
break;
// Change conversations status
case 'bulk_conversation_change_status':
$conversations = Conversation::findMany($request->conversation_id);
$new_status = (int) $request->status;
if (!in_array((int) $request->status, array_keys(Conversation::$statuses))) {
$response['msg'] = __('Incorrect status');
}
if (!$response['msg']) {
foreach ($conversations as $conversation) {
if (!$user->can('update', $conversation)) {
continue;
}
$conversation->changeStatus($new_status, $user);
}
$response['status'] = 'success';
// Flash
$flash_message = __('Status updated');
\Session::flash('flash_success_floating', $flash_message);
$response['msg'] = __('Status updated');
}
break;
// Delete converations.
case 'bulk_delete_conversation':
// At first, check if this user is able to delete conversations
if (!auth()->user()->isAdmin() && !auth()->user()->hasPermission(\App\User::PERM_DELETE_CONVERSATIONS)) {
$response['msg'] = __('Not enough permissions');
//\Session::flash('flash_success_floating', __('Conversations deleted'));
return \Response::json($response);
}
$conversations = Conversation::findMany($request->conversation_id);
$mailboxes_to_recalculate = [];
foreach ($conversations as $conversation) {
if (!$user->can('delete', $conversation)) {
continue;
}
if ($conversation->state != Conversation::STATE_DELETED) {
// Move to Deleted folder.
$conversation->state = Conversation::STATE_DELETED;
$conversation->user_updated_at = date('Y-m-d H:i:s');
$conversation->updateFolder();
$conversation->save();
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->user_id = $conversation->user_id;
$thread->type = Thread::TYPE_LINEITEM;
$thread->state = Thread::STATE_PUBLISHED;
$thread->status = Thread::STATUS_NOCHANGE;
$thread->action_type = Thread::ACTION_TYPE_DELETED_TICKET;
$thread->source_via = Thread::PERSON_USER;
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $conversation->customer_id;
$thread->created_by_user_id = $user->id;
$thread->save();
// Remove conversation from drafts folder.
$conversation->removeFromFolder(Folder::TYPE_DRAFTS);
} else {
// Delete forever
$conversation->deleteForever();
}
if (!array_key_exists($conversation->mailbox_id, $mailboxes_to_recalculate)) {
$mailboxes_to_recalculate[$conversation->mailbox_id] = $conversation->mailbox;
}
}
// Recalculate folders counters for mailboxes.
foreach ($mailboxes_to_recalculate as $mailbox) {
$mailbox->updateFoldersCounters();
}
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversations deleted'));
break;
// Delete converations in a specific folder.
case 'empty_folder':
// At first, check if this user is able to delete conversations
if (!auth()->user()->isAdmin() && !auth()->user()->hasPermission(\App\User::PERM_DELETE_CONVERSATIONS)) {
$response['msg'] = __('Not enough permissions');
return \Response::json($response);
}
$response = \Eventy::filter('conversations.empty_folder', $response,
$request->mailbox_id,
$request->folder_id
);
if (empty($response['processed'])) {
$folder = Folder::find($request->folder_id ?? '');
if (!$folder) {
$response['msg'] = __('Folder not found');
}
if (!$response['msg']) {
$conversation_ids = Conversation::where('folder_id', $folder->id)->pluck('id')->toArray();
Conversation::deleteConversationsForever($conversation_ids);
if ($folder->mailbox) {
Conversation::clearStarredByUserCache($user->id, $folder->mailbox_id);
$folder->mailbox->updateFoldersCounters();
} else {
$folder->updateCounters();
}
}
}
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversations deleted'));
break;
// Move conversation to another mailbox.
case 'conversation_move':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('update', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
$response['msg'] = __('Not enough permissions');
}
$mailbox = null;
if (!$response['msg']) {
if (!empty($request->mailbox_email)) {
$mailbox = Mailbox::where('email', $request->mailbox_email)->first();
} else {
$mailbox = Mailbox::find($request->mailbox_id);
}
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
}
}
if (!$response['msg']) {
$prev_folder_id = Conversation::getFolderParam();
$prev_mailbox_id = $conversation->mailbox_id;
$conversation->moveToMailbox($mailbox, $user);
// If user does not have access to the new mailbox,
// redirect to the previous mailbox.
if (!$mailbox->userHasAccess($user->id)) {
if (!empty($prev_folder_id)) {
$response['redirect_url'] = route('mailboxes.view.folder', ['id' => $prev_mailbox_id, 'folder_id' => $prev_folder_id]);
} else {
$response['redirect_url'] = route('mailboxes.view', ['id' => $prev_mailbox_id]);
}
}
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversation moved'));
}
break;
// Merge conversations
case 'conversation_merge':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('view', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!empty($request->merge_conversation_id) && is_array($request->merge_conversation_id)) {
$sigle_conv = count($request->merge_conversation_id) == 1;
foreach ($request->merge_conversation_id as $merge_conversation_id) {
$merge_conversation = Conversation::find($merge_conversation_id);
$response['msg'] = '';
if (!$merge_conversation) {
$response['msg'] = __('Conversation not found');
if ($sigle_conv) {
break;
}
}
if (!$response['msg'] && !$user->can('view', $merge_conversation)) {
$response['msg'] = __('Not enough permissions').': #'.$merge_conversation->number;
if ($sigle_conv) {
break;
}
}
if (!$response['msg']) {
$conversation->mergeConversations($merge_conversation, $user);
if ($response['status'] != 'success') {
\Session::flash('flash_success_floating', __('Conversations merged'));
}
$response['status'] = 'success';
}
}
}
break;
// Follow conversation
case 'follow':
case 'unfollow':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
$response['msg'] = __('Not enough permissions');
}
if ($request->action == 'follow') {
$user->followConversation($request->conversation_id);
} else {
$follower = Follower::where('conversation_id', $request->conversation_id)
->where('user_id', $user->id)
->first();
if ($follower) {
$follower->delete();
}
}
if (!$response['msg']) {
$response['status'] = 'success';
if ($request->action == 'follow') {
$response['msg_success'] = __('Following');
} else {
$response['msg_success'] = __('Unfollowed');
}
}
break;
case 'update_subject':
$conversation = Conversation::find($request->conversation_id);
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
$response['msg'] = __('Not enough permissions');
}
$subject = $request->value ?? '';
$subject = trim($subject);
if (!$response['msg'] && $subject) {
$conversation->subject = $subject;
$conversation->save();
$response['status'] = 'success';
}
break;
case 'merge_search':
$conversation = Conversation::where(Conversation::numberFieldName(), $request->number)->first();
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('view', $conversation)) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg']) {
$response['html'] = \View::make('conversations/partials/merge_search_result')->with([
'conversation' => $conversation
])->render();
$response['status'] = 'success';
}
break;
case 'chats_load_more':
$mailbox = Mailbox::find($request->mailbox_id);
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
} elseif (!$mailbox->userHasAccess($user->id)) {
$response['msg'] = __('Action not authorized');
}
if (!$response['msg']) {
$response['html'] = \View::make('mailboxes/partials/chat_list')->with([
'mailbox' => $mailbox,
'offset' => $request->offset,
])->render();
$response['status'] = 'success';
}
break;
case 'retry_send':
$thread = Thread::find($request->thread_id);
if (!$thread) {
$response['msg'] = __('Thread not found');
} elseif (!$user->can('view', $thread->conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$job_id = $thread->getFailedJobId();
if ($job_id) {
\App\FailedJob::retry($job_id);
$thread->send_status = SendLog::STATUS_ACCEPTED;
$thread->updateSendStatusData(['msg' => '']);
$thread->save();
$response['status'] = 'success';
}
}
break;
case 'load_customer_info':
$customer = Customer::getByEmail($request->customer_email);
if ($customer) {
// Previous conversations
$prev_conversations = [];
$mailbox = Mailbox::find($request->mailbox_id);
if ($mailbox && $mailbox->userHasAccess($user->id)) {
$conversation_id = (int)$request->conversation_id ?? 0;
$prev_conversations = $mailbox->conversations()
->where('customer_id', $customer->id)
->where('id', '<>', $conversation_id)
->where('status', '!=', Conversation::STATUS_SPAM)
->where('state', Conversation::STATE_PUBLISHED)
//->limit(self::PREV_CONVERSATIONS_LIMIT)
->orderBy('created_at', 'desc')
->paginate(self::PREV_CONVERSATIONS_LIMIT);
}
$response['html'] = \View::make('conversations/partials/customer_sidebar')->with([
'customer' => $customer,
'prev_conversations' => $prev_conversations,
])->render();
$response['status'] = 'success';
} else {
$response['msg'] = 'Customer not found';
}
break;
default:
$response['msg'] = 'Unknown action';
break;
}
if ($response['status'] == 'error' && empty($response['msg'])) {
$response['msg'] = 'Unknown error occurred';
}
return \Response::json($response);
}
/**
* Conversations ajax controller.
*/
public function ajaxHtml(Request $request)
{
switch ($request->action) {
case 'send_log':
return $this->ajaxHtmlSendLog();
case 'show_original':
return $this->ajaxHtmlShowOriginal();
case 'change_customer':
return $this->ajaxHtmlChangeCustomer();
case 'move_conv':
return $this->ajaxHtmlMoveConv();
case 'merge_conv':
return $this->ajaxHtmlMergeConv();
case 'default_redirect':
return $this->ajaxHtmlDefaultRedirect();
}
abort(404);
}
/**
* Send log.
*/
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,
]);
}
/**
* Show original message headers.
*/
public function ajaxHtmlShowOriginal()
{
$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);
}
$fetched = true;
$body_preview = $thread->body;
if ($thread->isCustomerMessage()) {
$fetched = false;
// Try to fetch original body by imap.
$body_imap = $thread->fetchBody();
if ($body_imap) {
$fetched = true;
$body_preview = $body_imap;
}
}
return view('conversations/ajax_html/show_original', [
'thread' => $thread,
'body_preview' => $body_preview,
'fetched' => $fetched,
]);
}
/**
* Change conversation customer.
*/
public function ajaxHtmlChangeCustomer()
{
$conversation_id = Input::get('conversation_id');
if (!$conversation_id) {
abort(404);
}
$conversation = Conversation::find($conversation_id);
if (!$conversation) {
abort(404);
}
$user = auth()->user();
if (!$user->can('view', $conversation)) {
abort(403);
}
return view('conversations/ajax_html/change_customer', [
'conversation' => $conversation,
]);
}
/**
* Move conversation to other mailbox.
*/
public function ajaxHtmlMoveConv()
{
$conversation_id = Input::get('conversation_id');
if (!$conversation_id) {
abort(404);
}
$conversation = Conversation::find($conversation_id);
if (!$conversation) {
abort(404);
}
$user = auth()->user();
if (!$user->can('view', $conversation)) {
abort(403);
}
return view('conversations/ajax_html/move_conv', [
'conversation' => $conversation,
'mailboxes' => $user->mailboxesCanView(),
]);
}
/**
* Merge conversations.
*/
public function ajaxHtmlMergeConv()
{
$conversation_id = Input::get('conversation_id');
if (!$conversation_id) {
abort(404);
}
$conversation = Conversation::find($conversation_id);
if (!$conversation) {
abort(404);
}
$user = auth()->user();
if (!$user->can('view', $conversation)) {
\Helper::denyAccess();
}
$prev_conversations = [];
if ($conversation->customer_id) {
$prev_conversations = $conversation->mailbox->conversations()
->where('customer_id', $conversation->customer_id)
->where('id', '<>', $conversation->id)
->where('status', '!=', Conversation::STATUS_SPAM)
->where('state', Conversation::STATE_PUBLISHED)
->orderBy('created_at', 'desc')
->paginate(500);
}
return view('conversations/ajax_html/merge_conv', [
'conversation' => $conversation,
'prev_conversations' => $prev_conversations,
]);
}
/**
* Change default redirect for the mailbox.
*/
public function ajaxHtmlDefaultRedirect()
{
$mailbox_id = Input::get('mailbox_id');
if (!$mailbox_id) {
abort(404);
}
$mailbox = Mailbox::find($mailbox_id);
if (!$mailbox) {
abort(404);
}
$user = auth()->user();
if (!$user->can('view', $mailbox)) {
abort(403);
}
return view('conversations/ajax_html/default_redirect', [
'after_send' => $user->mailboxSettings($mailbox_id)->after_send,
'mailbox_id' => $mailbox_id,
]);
}
/**
* Get redirect URL after performing an action.
*/
public function getRedirectUrl($request, $conversation, $user)
{
if (!empty($request->after_send)) {
$after_send = $request->after_send;
} else {
// todo: use $user->mailboxSettings()
$after_send = $conversation->mailbox->getUserSettings($user->id)->after_send;
}
// When creating a new conversation.
if (!empty($request->is_create) && $after_send != MailboxUser::AFTER_SEND_STAY) {
return route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $conversation->folder_id]);
}
// if ($conversation->state == Conversation::STATE_DRAFT) {
// return route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $conversation->folder_id]);
// }
if (!empty($after_send)) {
switch ($after_send) {
case MailboxUser::AFTER_SEND_STAY:
default:
$redirect_url = $conversation->url();
break;
case MailboxUser::AFTER_SEND_FOLDER:
$folder_id = Conversation::getFolderParam();
if (!$folder_id) {
$folder_id = $conversation->folder_id;
}
$redirect_url = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
break;
case MailboxUser::AFTER_SEND_NEXT:
// We need to get not any next conversation, but ACTIVE next conversation.
$redirect_url = $conversation->urlNext(Conversation::getFolderParam(), Conversation::STATUS_ACTIVE, true);
break;
}
} else {
// If something went wrong and after_send not set, just show the reply
$redirect_url = $conversation->url();
}
return $redirect_url;
}
/**
* Upload files and images.
*/
public function upload(Request $request)
{
$response = [
'status' => 'error',
'msg' => '', // this is error message
];
$user = auth()->user();
if (!$user) {
$response['msg'] = __('Please login to upload file');
}
if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) {
$response['msg'] = __('Error occurred uploading file');
}
if (!$response['msg']) {
$embedded = true;
if (!empty($request->attach) && (int) $request->attach) {
$embedded = false;
}
$attachment = Attachment::create(
$request->file->getClientOriginalName(),
$request->file->getMimeType(),
null,
'',
$request->file,
$embedded,
null,
$user->id
);
if ($attachment) {
$response['status'] = 'success';
$response['url'] = $attachment->url();
$response['attachment_id'] = $attachment->id;
} else {
$response['msg'] = __('Error occurred uploading file');
}
}
return \Response::json($response);
}
/**
* Ajax conversation navigation.
*/
public function ajaxConversationsPagination(Request $request, $response, $user)
{
//$mailbox = Mailbox::find($request->mailbox_id);
$folder = null;
$conversations = [];
if (!$response['msg']) {
$folder = \Eventy::filter('conversations.ajax_pagination_folder', Folder::find($request->folder_id), $request, $response, $user);
if (!$folder) {
$response['msg'] = __('Folder not found');
}
}
if (!$response['msg'] && !$user->can('view', $folder)) {
$response['msg'] = __('Not enough permissions');
}
// We should not use mailbox_id from the request, as it can be changed.
if (!$response['msg'] && !$user->can('view', $folder->mailbox)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->page);
}
$response['status'] = 'success';
$response['html'] = view('conversations/conversations_table', [
'folder' => $folder,
'conversations' => $conversations,
])->render();
return $response;
}
/**
* Search.
*/
public function search(Request $request)
{
$user = auth()->user();
$conversations = [];
$customers = [];
$mode = $this->getSearchMode($request);
// Search query
$q = $this->getSearchQuery($request);
// Filters.
$filters = $this->getSearchFilters($request);
$filters_data = [];
// Modify filters is needed.
if (!empty($filters['customer'])) {
// Get customer name.
$filters_data['customer'] = Customer::find($filters['customer']);
}
//$filters = \Eventy::filter('search.filters', $filters, $filters_data, $mode, $q);
// Remember recent query.
$recent_search_queries = session('recent_search_queries') ?? [];
if ($q && !in_array($q, $recent_search_queries)) {
array_unshift($recent_search_queries, $q);
$recent_search_queries = array_slice($recent_search_queries, 0, 4);
session()->put('recent_search_queries', $recent_search_queries);
}
$conversations = [];
if (\Eventy::filter('search.is_needed', true, 'conversations')) {
$conversations = $this->searchQuery($user, $q, $filters);
}
// Jump to the conversation if searching by conversation number.
if (count($conversations) == 1
&& $conversations[0]->number == $q
&& empty($filters)
&& !$request->x_embed
) {
return redirect()->away($conversations[0]->url($conversations[0]->folder_id));
}
$customers = $this->searchCustomers($request, $user);
// Dummy folder
$folder = $this->getSearchFolder($conversations);
// List of available filters.
if ($mode == Conversation::SEARCH_MODE_CONV) {
$filters_list = \Eventy::filter('search.filters_list', Conversation::$search_filters, $mode, $filters, $q);
} else {
$filters_list = \Eventy::filter('search.filters_list_customers', Customer::$search_filters, $mode, $filters, $q);
}
$mailboxes = \Cache::remember('search_filter_mailboxes_'.$user->id, 5, function () use ($user) {
return $user->mailboxesCanView();
});
$users = \Cache::remember('search_filter_users_'.$user->id, 5, function () use ($user, $mailboxes) {
return \Eventy::filter('search.assignees', $user->whichUsersCanView($mailboxes), $user, $mailboxes);
});
$search_mailbox = null;
if (isset($filters['mailbox'])) {
$mailbox_id = (int)$filters['mailbox'];
if ($mailbox_id && in_array($mailbox_id, $mailboxes->pluck('id')->toArray())) {
foreach ($mailboxes as $mailbox_item) {
if ($mailbox_item->id == $mailbox_id) {
$search_mailbox = $mailbox_item;
break;
}
}
}
} elseif (count($mailboxes) == 1) {
$search_mailbox = $mailboxes[0];
}
return view('conversations/search', [
'folder' => $folder,
'q' => $request->q,
'filters' => $filters,
'filters_list' => $filters_list,
'filters_data' => $filters_data,
//'filters_list_all' => $filters_list_all,
'mode' => $mode,
'conversations' => $conversations,
'customers' => $customers,
'recent' => session('recent_search_queries'),
'users' => $users,
'mailboxes' => $mailboxes,
'search_mailbox' => $search_mailbox,
]);
}
/**
* Search conversations.
*/
public function getSearchMode($request)
{
$mode = Conversation::SEARCH_MODE_CONV;
if (!empty($request->mode) && $request->mode == Conversation::SEARCH_MODE_CUSTOMERS) {
$mode = Conversation::SEARCH_MODE_CUSTOMERS;
}
return $mode;
}
/**
* Search conversations.
*/
public function searchQuery($user, $q, $filters)
{
$conversations = \Eventy::filter('search.conversations.perform', '', $q, $filters, $user);
if ($conversations !== '') {
return $conversations;
}
$query_conversations = Conversation::search($q, $filters, $user);
return $query_conversations->paginate(Conversation::DEFAULT_LIST_SIZE);
}
/**
* Get and format search query.
*/
public function getSearchQuery($request)
{
$q = '';
if (!empty($request->q)) {
$q = $request->q;
} elseif (!empty($request->filter) && !empty($request->filter['q'])) {
$q = $request->filter['q'];
}
return trim($q);
}
/**
* Get and format search filters.
*/
public function getSearchFilters($request)
{
$filters = [];
if (!empty($request->f)) {
$filters = $request->f;
} elseif (!empty($request->filter) && !empty($request->filter['f'])) {
$filters = $request->filter['f'];
}
foreach ($filters as $filter => $value) {
switch ($filter) {
case 'after':
case 'before':
if ($value) {
$filters[$filter] = date('Y-m-d', strtotime($value));
}
break;
}
}
$filters = \Eventy::filter('search.filters', $filters, $this->getSearchMode($request), $request);
return $filters;
}
/**
* Search conversations.
*/
public function searchCustomers($request, $user)
{
// Get IDs of mailboxes to which user has access
$mailbox_ids = $user->mailboxesIdsCanView();
// Filters
$filters = $this->getSearchFilters($request);;
// Search query
$q = $this->getSearchQuery($request);
// Like is case insensitive.
$like = '%'.mb_strtolower($q).'%';
// We need to use aggregate function for email to avoid "Grouping error" error in PostgreSQL.
$query_customers = Customer::select(['customers.*', \DB::raw('MAX(emails.email)')])
->groupby('customers.id')
->leftJoin('emails', function ($join) {
$join->on('customers.id', '=', 'emails.customer_id');
})
->where(function ($query) use ($like, $q) {
$like_op = 'like';
if (\Helper::isPgSql()) {
$like_op = 'ilike';
}
$query->where('customers.first_name', $like_op, $like)
->orWhere('customers.last_name', $like_op, $like)
->orWhere('customers.company', $like_op, $like)
->orWhere('customers.job_title', $like_op, $like)
->orWhere('customers.websites', $like_op, $like)
->orWhere('customers.social_profiles', $like_op, $like)
->orWhere('customers.address', $like_op, $like)
->orWhere('customers.city', $like_op, $like)
->orWhere('customers.state', $like_op, $like)
->orWhere('customers.zip', $like_op, $like)
->orWhere('emails.email', $like_op, $like);
$phone_numeric = \Helper::phoneToNumeric($q);
if ($phone_numeric) {
$query->orWhere('customers.phones', $like_op, '%"'.$phone_numeric.'"%');
}
});
if (!empty($filters['mailbox']) && in_array($filters['mailbox'], $mailbox_ids)) {
$query_customers->join('conversations', function ($join) use ($filters) {
$join->on('conversations.customer_id', '=', 'customers.id');
//$join->on('conversations.mailbox_id', '=', $filters['mailbox']);
});
$query_customers->where('conversations.mailbox_id', '=', $filters['mailbox']);
}
$query_customers = \Eventy::filter('search.customers.apply_filters', $query_customers, $filters, $q);
return $query_customers->paginate(50);
}
/**
* Get dummy folder for search.
*/
public function getSearchFolder($conversations)
{
$folder = new Folder();
$folder->type = Folder::TYPE_ASSIGNED;
// todo: use select([\DB::raw('SQL_CALC_FOUND_ROWS *')]) to count records
//$folder->total_count = $conversations->count();
return $folder;
}
/**
* Ajax conversations search.
*/
public function ajaxConversationsFilter(Request $request, $response, $user)
{
if (array_key_exists('q', $request->filter)) {
// Search.
$conversations = $this->searchQuery($user, $this->getSearchQuery($request), $this->getSearchFilters($request));
} else {
// Filters in the mailbox or customer profile.
$conversations = $this->conversationsFilterQuery($request, $user);
}
$response['status'] = 'success';
$response['html'] = view('conversations/conversations_table', [
'conversations' => $conversations,
'params' => $request->params ?? [],
])->render();
return $response;
}
/**
* Filter conversations according to the request.
*/
public function conversationsFilterQuery($request, $user)
{
// Get IDs of mailboxes to which user has access
$mailbox_ids = $user->mailboxesIdsCanView();
$query_conversations = Conversation::whereIn('conversations.mailbox_id', $mailbox_ids)
->orderBy('conversations.last_reply_at');
foreach ($request->filter as $field => $value) {
switch ($field) {
case 'customer_id':
$query_conversations->where('customer_id', $value);
break;
}
}
return $query_conversations->paginate(Conversation::DEFAULT_LIST_SIZE);
}
/**
* Process attachments on reply, new conversation, saving draft and forwarding.
*/
public function processReplyAttachments($request)
{
$has_attachments = false;
$attachments = [];
if (!empty($request->attachments_all)) {
$embeds = [];
if (!empty($request->attachments)) {
$attachments = $request->attachments;
}
if (!empty($request->embeds)) {
$embeds = $request->embeds;
}
if (count($attachments) != count($embeds)) {
$has_attachments = true;
}
$attachments_to_remove = array_diff($request->attachments_all, $attachments);
$attachments_to_remove = array_diff($attachments_to_remove, $embeds);
Attachment::deleteByIds($attachments_to_remove);
}
return [
'has_attachments' => $has_attachments,
'attachments' => $attachments,
];
}
/**
* Undo reply.
*/
public function undoReply(Request $request, $thread_id)
{
$thread = Thread::findOrFail($thread_id);
if (!$thread) {
abort(404);
}
$conversation = $thread->conversation;
$this->authorize('view', $conversation);
// Check undo timeout
if ($thread->created_at->diffInSeconds(now()) > Conversation::UNDO_TIMOUT) {
\Session::flash('flash_error_floating', __('Sending can not be undone'));
return redirect()->away($conversation->url($conversation->folder_id));
}
// Convert reply into draft
$thread->state = Thread::STATE_DRAFT;
$thread->save();
// https://github.com/freescout-helpdesk/freescout/issues/3300
// Cancel all SendReplyToCustomer jobs for this thread.
$jobs_to_cancel = \App\Job::where('queue', 'emails')
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
->get();
foreach ($jobs_to_cancel as $job) {
$job_thread = $job->getCommandLastThread();
if ($job_thread && $job_thread->id == $thread->id) {
$job->delete();
}
}
// Get penultimate reply
$last_thread = $conversation->threads()
->where('id', '<>', $thread->id)
->whereIn('type', [Thread::TYPE_CUSTOMER, Thread::TYPE_MESSAGE])
->orderBy('created_at', 'desc')
->first();
$folder_id = $conversation->folder_id;
// Restore conversation data from penultimate thread
if ($last_thread) {
$conversation->setCc($last_thread->cc);
$conversation->setBcc($last_thread->bcc);
$conversation->last_reply_at = $last_thread->created_at;
$conversation->last_reply_from = $last_thread->source_via;
$conversation->user_updated_at = date('Y-m-d H:i:s');
}
if ($thread->first) {
// This was a new conversation, move it to drafts
$conversation->state = Thread::STATE_DRAFT;
$conversation->updateFolder();
$conversation->mailbox->updateFoldersCounters();
$folder_id = null;
}
$conversation->save();
// If forwarding has been undone, we need to remove newly created conversation.
// No need to remove notifications, as they won't work if conversation does not exist.
if ($thread->isForward()) {
$forwarded_conversation = $thread->getForwardChildConversation();
if ($forwarded_conversation) {
$forwarded_conversation->threads()->delete();
// todo: maybe perform soft delete of the conversation.
$forwarded_conversation->delete();
}
}
Conversation::updatePreview($conversation->id);
return redirect()->away($conversation->url($folder_id, null, ['show_draft' => $thread->id]));
}
/**
* Find or create customer when creating a Phone conversation.
*/
public function processPhoneCustomer($request)
{
$customer_data = [];
$customer_email = '';
$customer = null;
// Check to prevent creating empty customers.
$request_name = '';
$request_phone = '';
if (trim($request->name ?? '') || trim($request->phone ?? '')) {
$request_name = trim($request->name ?? '');
$request_phone = trim($request->phone ?? '');
$name_parts = explode(' ', $request_name);
$customer_data['first_name'] = $name_parts[0];
if (!empty($name_parts[1])) {
$customer_data['last_name'] = $name_parts[1];
}
$customer_data['phones'] = [$request_phone];
}
// Check if name field contains ID of the customer.
if (!$request->customer_id && is_numeric($request_name)) {
// Try to find customer by ID.
$customer = Customer::find($request_name);
}
if (!$customer && $request->to_email) {
// Try to get customer by email.
$customer = Customer::getByEmail($request->to_email);
if ($customer) {
$customer_email = $request->to_email;
}
}
// Try to find customer by phone.
if (!$customer && $request_phone) {
$customer = Customer::findByPhone($request_phone);
if ($customer) {
$customer_email = $customer->getMainEmail();
}
}
if (!$customer) {
// Create customer with passed name, email and phone
if (Email::sanitizeEmail($request->to_email)) {
$customer_email = $request->to_email;
// If new email entered, attach email to the current customer
// instead of creating a new customer
if ($request->customer_id) {
$customer = Customer::find($request->customer_id);
if ($customer) {
// Add email to customer.
$customer->addEmail($customer_email, true);
} else {
$customer = Customer::create($customer_email, $customer_data);
}
} else {
$customer = Customer::create($customer_email, $customer_data);
}
} elseif ($customer_data) {
if ($request->customer_id) {
$customer = Customer::find($request->customer_id);
if ($customer) {
$customer->setData($customer_data, false, true);
}
}
if (!$customer) {
$customer = Customer::createWithoutEmail($customer_data);
}
}
} else {
$customer->setData($customer_data, false, true);
// Add email to customer.
if (Email::sanitizeEmail($request->to_email)) {
$customer->addEmail($request->to_email, true);
}
}
return [
'customer' => $customer,
'customer_email' => $customer_email,
];
}
/**
* View conversation.
*/
public function chats(Request $request, $mailbox_id)
{
$user = auth()->user();
$mailbox = Mailbox::findOrFailWithSettings($mailbox_id, $user->id);
$this->authorize('viewCached', $mailbox);
// Redirect to the first available chat.
$chats = Conversation::getChats($mailbox_id, 0, 1);
if (count($chats)) {
if (!\Helper::isChatMode()) {
\Helper::setChatMode(true);
}
return redirect()->away($chats[0]->url());
}
return view('conversations/chats', [
'is_in_chat_mode' => true,
'mailbox' => $mailbox,
]);
}
}