*/ namespace Modules\MMFRestrictedCustomers\Http\Controllers; use Validator; use Illuminate\Http\Request; use App\Attachment; use App\Conversation; use App\Events\ConversationStatusChanged; use App\Events\ConversationUserChanged; use App\Events\UserAddedNote; use App\Events\UserCreatedConversation; use App\Events\UserReplied; use App\Mailbox; use App\MailboxUser; use App\Thread; use Modules\MMFRestrictedCustomers\Entities\Customer; use App\Http\Controllers\ConversationsController as BaseConversationsController; class ConversationsController extends BaseConversationsController { /** * Conversations ajax controller. */ public function ajax(Request $request) { // This override is only required for "send_reply" requests. if ( $request->action != 'send_reply' ) return parent::ajax($request); $response = [ 'status' => 'error', 'msg' => '', // this is error message ]; $user = auth()->user(); $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_custom = false; if ($type == Conversation::TYPE_CUSTOM) { $is_custom = 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 || $is_custom ) && ! $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', ]); } elseif ($type === Conversation::TYPE_PHONE) { // 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', ]); } elseif ($type === Conversation::TYPE_CUSTOM) { $validation_rules = \Eventy::filter('conversation.custom.validation_rules', [ 'body' => 'required|string', 'cc' => 'nullable|array', 'bcc' => 'nullable|array', ], $request); $validator = Validator::make($request->all(), $validation_rules); } } 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 && ! $is_custom) { 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 ?? []); $attachments_ids = $this->decodeAttachmentsIds($attachments_ids); 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; // Chat conversations in chat mode can not be undone. $can_undo = true; $request_status = (int)$request->status; 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 ($request_status && $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; } } elseif ($is_custom) { // No customer for custom conversations. } 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) { // The new Customer must be linked to a specific Mailbox. $data = []; $data['mailbox_id'] = $conversation->mailbox_id; $customer = Customer::create($customer_email, $data); $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 ?: $conversation->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.'); return \Response::json($response); } $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 ?? $conversation->status; $thread->state = Thread::STATE_PUBLISHED; if (!$is_custom) { $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); } if ($conversation->isChat() && \Helper::isChatMode()) { $can_undo = false; } // 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($this->getUndoTimeout($can_undo))); } 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($this->getUndoTimeout($can_undo))); } // 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($this->getUndoTimeout($can_undo))); } } // 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_custom) { $flash_type = 'warning'; $identifier = \Eventy::filter('conversation.custom.identifier', __('Custom conversation'), $request); if ($show_view_link) { $flash_text = __(':%tag_start%' . $identifier . ' added:%tag_end% :%view_start%View:%a_end%', $flash_vars); } else { $flash_text = ''.__('%identifier% added',['%identifier%'=>$identifier]).''; } } 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); } } if ($can_undo) { \Session::flash('flash_'.$flash_type.'_floating', $flash_text); } } if ($response['status'] == 'error' && empty($response['msg'])) { $response['msg'] = 'Unknown error occurred'; } return \Response::json($response); } }