diff --git a/app/Attachment.php b/app/Attachment.php new file mode 100644 index 00000000..95fdd803 --- /dev/null +++ b/app/Attachment.php @@ -0,0 +1,148 @@ + self::TYPE_MESSAGE, + 'application' => self::TYPE_APPLICATION, + 'audio' => self::TYPE_AUDIO, + 'image' => self::TYPE_IMAGE, + 'video' => self::TYPE_VIDEO, + 'model' => self::TYPE_MODEL, + 'text' => self::TYPE_TEXT, + 'multipart' => self::TYPE_MULTIPART, + 'other' => self::TYPE_OTHER, + ]; + + const DIRECTORY = 'attachment'; + + public $timestamps = false; + + /** + * Get thread. + */ + public function thread() + { + return $this->belongsTo('App\Thread'); + } + + /** + * Save attachment to file and database. + */ + public static function create($name, $mime_type, $type, $content, $thread_id = null) + { + if (!$content) { + return false; + } + + $file_name = $name; + + $attachment = new Attachment(); + $attachment->thread_id = $thread_id; + $attachment->name = $name; + $attachment->file_name = $file_name; + $attachment->mime_type = $mime_type; + $attachment->type = $type; + //$attachment->size = Storage::size($file_path); + $attachment->save(); + + $file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.self::getPath($attachment->id).$file_name; + Storage::put($file_path, $content); + + $attachment->size = Storage::size($file_path); + if ($attachment->size) { + $attachment->save(); + } + return true; + } + + /** + * Get file path by ID. + * + * @param integer $id + * @return string + */ + public static function getPath($id) + { + $hash = md5($id); + + $first = -1; + $second = 0; + + for ($i = 0; $i < strlen($hash); $i++) { + if (is_numeric($hash[$i])) { + if ($first == -1) { + $first = $hash[$i]; + } else { + $second = $hash[$i]; + break; + } + } + } + if ($first == -1) { + $first = 0; + } + return $first.DIRECTORY_SEPARATOR.$second.DIRECTORY_SEPARATOR; + } + + /** + * Conver type name to integer. + */ + public static function typeNameToInt($type_name) + { + if (!empty(self::$types[$type_name])) { + return self::$types[$type_name]; + } else { + return self::TYPE_OTHER; + } + } + + /** + * Get attachment public URL. + * + * @return string + */ + public function getUrl() + { + return Storage::url(self::DIRECTORY.DIRECTORY_SEPARATOR.self::getPath($this->id).$this->file_name); + } + + /** + * Convert size into human readable format. + * + * @return string + */ + public function getSizeName() + { + return self::formatBytes($this->size); + } + + public static function formatBytes($size, $precision = 0) + { + $size = (int) $size; + if ($size > 0) { + $base = log($size) / log(1024); + $suffixes = array(' b', ' KB', ' MB', ' GB', ' TB'); + + return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)]; + } else { + return $size; + } + } +} diff --git a/app/Console/Commands/FetchEmails.php b/app/Console/Commands/FetchEmails.php index 50531503..87c48198 100644 --- a/app/Console/Commands/FetchEmails.php +++ b/app/Console/Commands/FetchEmails.php @@ -2,6 +2,7 @@ namespace App\Console\Commands; +use App\Attachment; use App\Conversation; use App\Customer; use App\Email; @@ -28,6 +29,13 @@ class FetchEmails extends Command */ protected $description = 'Fetch emails from mailboxes addresses'; + /** + * Current mailbox. + * + * @var Mailbox + */ + public $mailbox; + /** * Create a new command instance. * @@ -56,17 +64,12 @@ class FetchEmails extends Command foreach ($mailboxes as $mailbox) { $this->info('['.date('Y-m-d H:i:s').'] Mailbox: '.$mailbox->name); + $this->mailbox = $mailbox; + try { $this->fetch($mailbox); } catch (\Exception $e) { - $this->error('['.date('Y-m-d H:i:s').'] Error: '.$e->getMessage().'; Line: '.$e->getLine()); - activity() - ->withProperties([ - 'error' => $e->getMessage(), - 'mailbox' => $mailbox->name, - ]) - ->useLog(\App\ActivityLog::NAME_EMAILS_FETCHING) - ->log(\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR); + $this->logError('Error: '.$e->getMessage().'; Line: '.$e->getLine()); } } } @@ -96,6 +99,10 @@ class FetchEmails extends Command // Get unseen messages for a period $messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get(); + if ($client->getLastError()) { + $this->logError($client->getLastError()); + } + $this->line('['.date('Y-m-d H:i:s').'] Fetched: '.count($messages)); $message_index = 1; @@ -119,7 +126,7 @@ class FetchEmails extends Command $body = $this->separateReply($body, false); } if (!$body) { - $this->error('['.date('Y-m-d H:i:s').'] Message body is empty'); + $this->logError('Message body is empty'); $message->setFlag(['Seen']); continue; } @@ -130,7 +137,7 @@ class FetchEmails extends Command $from = $message->getFrom(); } if (!$from) { - $this->error('['.date('Y-m-d H:i:s').'] From is empty'); + $this->logError('From is empty'); $message->setFlag(['Seen']); continue; } else { @@ -158,11 +165,29 @@ class FetchEmails extends Command $message->setFlag(['Seen']); $this->line('['.date('Y-m-d H:i:s').'] Processed'); } else { - $this->error('['.date('Y-m-d H:i:s').'] Error occured processing message'); + $this->logError('Error occured processing message'); } } } + public function logError($message) + { + $this->error('['.date('Y-m-d H:i:s').'] '.$message); + + $mailbox_name = ''; + if ($this->mailbox) { + $mailbox_name = $this->mailbox->name; + } + + activity() + ->withProperties([ + 'error' => $message, + 'mailbox' => $mailbox_name, + ]) + ->useLog(\App\ActivityLog::NAME_EMAILS_FETCHING) + ->log(\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR); + } + /** * Save email as thread. */ @@ -195,7 +220,6 @@ class FetchEmails extends Command $conversation = new Conversation(); $conversation->type = Conversation::TYPE_EMAIL; - $conversation->status = Conversation::STATUS_ACTIVE; $conversation->state = Conversation::STATE_PUBLISHED; $conversation->subject = $subject; $conversation->setCc($cc); @@ -210,6 +234,8 @@ class FetchEmails extends Command $conversation->source_via = Conversation::PERSON_CUSTOMER; $conversation->source_type = Conversation::SOURCE_TYPE_EMAIL; } + // Reply from customer makes conversation active + $conversation->status = Conversation::STATUS_ACTIVE; $conversation->last_reply_at = $now; $conversation->last_reply_from = Conversation::PERSON_USER; // Set folder id @@ -233,11 +259,42 @@ class FetchEmails extends Command $thread->created_by_customer_id = $customer->id; $thread->save(); + $has_attachments = $this->saveAttachments($attachments, $thread->id); + if ($has_attachments) { + $thread->has_attachments = true; + $thread->save(); + } + event(new CustomerReplied($conversation, $thread, $new)); return true; } + /** + * Save attachments from email. + * + * @param array $attachments + * @param integer $thread_id + * @return bool + */ + public function saveAttachments($email_attachments, $thread_id) + { + $has_attachments = false; + foreach ($email_attachments as $email_attachment) { + $create_result = Attachment::create( + $email_attachment->getName(), + $email_attachment->getMimeType(), + Attachment::typeNameToInt($email_attachment->getType()), + $email_attachment->getContent(), + $thread_id + ); + if ($create_result) { + $has_attachments = true; + } + } + return $has_attachments; + } + /** * Separate reply in the body. * diff --git a/app/Conversation.php b/app/Conversation.php index b2fbd357..2dde91b1 100644 --- a/app/Conversation.php +++ b/app/Conversation.php @@ -220,6 +220,18 @@ class Conversation extends Model } $text = strip_tags($text); + $text = trim(preg_replace('/\s+/', ' ', $text)); + + // Remove "undetectable" whitespaces + $whitespaces = ["%81", "%7F", "%C5%8D", "%8D", "%8F", "%C2%90", "%C2", "%90", "%9D", "%C2%A0", "%A0", "%C2%AD", "%AD", "%08", "%09", "%0A", "%0D"]; + $text = urlencode($text); + foreach($whitespaces as $char){ + $text = str_replace($char, ' ', $text); + } + $text = urldecode($text); + + $text = trim(preg_replace('/[ ]+/', ' ', $text)); + $this->preview = mb_substr($text, 0, self::PREVIEW_MAXLENGTH); return $this->preview; diff --git a/app/Thread.php b/app/Thread.php index 32c211b4..a66255d4 100644 --- a/app/Thread.php +++ b/app/Thread.php @@ -171,6 +171,14 @@ class Thread extends Model return $this->belongsTo('App\Conversation'); } + /** + * Get thread attachmets. + */ + public function attachments() + { + return $this->hasMany('App\Attachment'); + } + /** * Get user who created the thread. */ diff --git a/config/filesystems.php b/config/filesystems.php index 6b70115c..abfb4b57 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -44,8 +44,10 @@ return [ 'disks' => [ 'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', ], 'public' => [ diff --git a/database/migrations/2018_07_12_003318_create_threads_table.php b/database/migrations/2018_07_12_003318_create_threads_table.php index d568e8f3..99dd932e 100644 --- a/database/migrations/2018_07_12_003318_create_threads_table.php +++ b/database/migrations/2018_07_12_003318_create_threads_table.php @@ -33,6 +33,7 @@ class CreateThreadsTable extends Migration $table->text('to')->nullable(); // JSON $table->text('cc')->nullable(); // JSON $table->text('bcc')->nullable(); // JSON + $table->boolean('has_attachments')->default(false); // Email Message-ID header for email received from customer $table->string('message_id', 998)->nullable(); // source.via - Originating source of the thread - user or customer diff --git a/database/migrations/2018_08_04_063414_create_attachments_table.php b/database/migrations/2018_08_04_063414_create_attachments_table.php new file mode 100644 index 00000000..0221569a --- /dev/null +++ b/database/migrations/2018_08_04_063414_create_attachments_table.php @@ -0,0 +1,36 @@ +increments('id'); + $table->integer('thread_id')->nullable(); + $table->string('name', 255)->nullable(); + $table->string('file_name', 255); + $table->string('mime_type', 127); + $table->unsignedInteger('type'); + $table->unsignedInteger('size')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('attachments'); + } +} diff --git a/public/css/style.css b/public/css/style.css index 318e5679..fe23e8a2 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1328,6 +1328,35 @@ table.table-conversations td.conv-attachment { .thread-lineitem .thread-header { min-height: auto; } +.thread-attachments { + position: relative; +} +.thread-attachments ul { + list-style: none; + padding-left: 23px; +} +.thread-attachments li { + border-radius: 4px; + border: 1px solid #dadada; + display: inline-block; + line-height: 1; + background-color: #f5f5f5; + margin: 8px 4px 8px 0; + padding: 4px 10px; + position: relative; +} +.thread-attachments i { + color: #a5b2bd; + font-size: 14px; + position: absolute; + top: 12px; +} +.thread-attachments a { + display: inline-block; +} +.thread-attachments span { + text-transform: lowercase; +} .conv-new #conv-toolbar { border-bottom: 1px solid #d4d9dd; padding: 6px 0 7px 0; @@ -1934,4 +1963,21 @@ a.text-danger, a.text-danger:hover, a.text-danger:focus { color: #cc1f19!important; +} +.break-words { + /* These are technically the same, but use both */ + overflow-wrap: break-word; + word-wrap: break-word; + + -ms-word-break: break-all; + /* This is the dangerous one in WebKit, as it breaks things wherever */ + word-break: break-all; + /* Instead use this non-standard one: */ + word-break: break-word; + + /* Adds a hyphen where the word breaks, if supported (No Blink) */ + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; } \ No newline at end of file diff --git a/resources/views/conversations/view.blade.php b/resources/views/conversations/view.blade.php index 306ae396..cdcf5b06 100644 --- a/resources/views/conversations/view.blade.php +++ b/resources/views/conversations/view.blade.php @@ -262,6 +262,19 @@