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, ]); } }