mirror of
https://github.com/freescout-helpdesk/freescout.git
synced 2024-11-24 03:12:46 +01:00
Fetch email attachments
This commit is contained in:
parent
d93925c870
commit
ea7e6be8ee
148
app/Attachment.php
Normal file
148
app/Attachment.php
Normal file
@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Attachment extends Model
|
||||
{
|
||||
const TYPE_TEXT = 0;
|
||||
const TYPE_MULTIPART = 1;
|
||||
const TYPE_MESSAGE = 2;
|
||||
const TYPE_APPLICATION = 3;
|
||||
const TYPE_AUDIO = 4;
|
||||
const TYPE_IMAGE = 5;
|
||||
const TYPE_VIDEO = 6;
|
||||
const TYPE_MODEL = 7;
|
||||
const TYPE_OTHER = 8;
|
||||
|
||||
// https://github.com/Webklex/laravel-imap/blob/master/src/IMAP/Attachment.php
|
||||
public static $types = [
|
||||
'message' => 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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' => [
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateAttachmentsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('attachments', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
46
public/css/style.css
vendored
46
public/css/style.css
vendored
@ -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;
|
||||
}
|
@ -262,6 +262,19 @@
|
||||
<div class="thread-body">
|
||||
{!! $thread->getCleanBody() !!}
|
||||
</div>
|
||||
@if ($thread->has_attachments)
|
||||
<div class="thread-attachments">
|
||||
<i class="glyphicon glyphicon-paperclip"></i>
|
||||
<ul>
|
||||
@foreach ($thread->attachments as $attachment)
|
||||
<li>
|
||||
<a href="{{ $attachment->getUrl() }}" class="break-words" target="_blank">{{ $attachment->name }}</a>
|
||||
<span class="text-help">{{ $attachment->getSizeName() }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="dropdown thread-options">
|
||||
<span class="dropdown-toggle glyphicon glyphicon-option-vertical" data-toggle="dropdown"></span>
|
||||
|
3
vendor/composer/autoload_classmap.php
vendored
3
vendor/composer/autoload_classmap.php
vendored
@ -15,9 +15,10 @@ return array(
|
||||
'App\\Conversation' => $baseDir . '/app/Conversation.php',
|
||||
'App\\Customer' => $baseDir . '/app/Customer.php',
|
||||
'App\\Email' => $baseDir . '/app/Email.php',
|
||||
'App\\Events\\ConversationCreated' => $baseDir . '/app/Events/ConversationCreated.php',
|
||||
'App\\Events\\ConversationStatusChanged' => $baseDir . '/app/Events/ConversationStatusChanged.php',
|
||||
'App\\Events\\ConversationUserChanged' => $baseDir . '/app/Events/ConversationUserChanged.php',
|
||||
'App\\Events\\CustomerReplied' => $baseDir . '/app/Events/CustomerReplied.php',
|
||||
'App\\Events\\UserReplied' => $baseDir . '/app/Events/UserReplied.php',
|
||||
'App\\Exceptions\\Handler' => $baseDir . '/app/Exceptions/Handler.php',
|
||||
'App\\FailedJob' => $baseDir . '/app/FailedJob.php',
|
||||
'App\\Folder' => $baseDir . '/app/Folder.php',
|
||||
|
3
vendor/composer/autoload_static.php
vendored
3
vendor/composer/autoload_static.php
vendored
@ -406,9 +406,10 @@ class ComposerStaticInit992853026b5d9a5aba1df559a69505cb
|
||||
'App\\Conversation' => __DIR__ . '/../..' . '/app/Conversation.php',
|
||||
'App\\Customer' => __DIR__ . '/../..' . '/app/Customer.php',
|
||||
'App\\Email' => __DIR__ . '/../..' . '/app/Email.php',
|
||||
'App\\Events\\ConversationCreated' => __DIR__ . '/../..' . '/app/Events/ConversationCreated.php',
|
||||
'App\\Events\\ConversationStatusChanged' => __DIR__ . '/../..' . '/app/Events/ConversationStatusChanged.php',
|
||||
'App\\Events\\ConversationUserChanged' => __DIR__ . '/../..' . '/app/Events/ConversationUserChanged.php',
|
||||
'App\\Events\\CustomerReplied' => __DIR__ . '/../..' . '/app/Events/CustomerReplied.php',
|
||||
'App\\Events\\UserReplied' => __DIR__ . '/../..' . '/app/Events/UserReplied.php',
|
||||
'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
|
||||
'App\\FailedJob' => __DIR__ . '/../..' . '/app/FailedJob.php',
|
||||
'App\\Folder' => __DIR__ . '/../..' . '/app/Folder.php',
|
||||
|
Loading…
Reference in New Issue
Block a user