From 930a6b26af04aecc250d6e6e6ffe10fc751d7ad2 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 29 Jan 2014 18:28:52 +0200 Subject: [PATCH] inbound emails --- application/Espo/Entities/Email.php | 10 + application/Espo/Hooks/Common/Stream.php | 2 +- .../Modules/Crm/Controllers/InboundEmail.php | 12 + .../layouts/InboundEmail/detail.json | 13 +- .../metadata/entityDefs/InboundEmail.json | 5 +- .../Espo/Modules/Crm/Services/Activities.php | 33 ++- .../Modules/Crm/Services/InboundEmail.php | 241 ++++++++++++++++++ application/Espo/ORM/Entity.php | 9 + application/Espo/ORM/Repositories/RDB.php | 4 +- .../Resources/metadata/entityDefs/Email.json | 9 + application/Espo/Services/Record.php | 19 +- application/Espo/Services/Stream.php | 20 ++ 12 files changed, 347 insertions(+), 30 deletions(-) create mode 100644 application/Espo/Modules/Crm/Services/InboundEmail.php diff --git a/application/Espo/Entities/Email.php b/application/Espo/Entities/Email.php index 0a834b51d5..6d3fdf27cf 100644 --- a/application/Espo/Entities/Email.php +++ b/application/Espo/Entities/Email.php @@ -5,4 +5,14 @@ namespace Espo\Entities; class Email extends \Espo\Core\ORM\Entity { + protected function getSubject() + { + return $this->get('name'); + } + + protected function setSubject($value) + { + return $this->set('name', $value); + } } + diff --git a/application/Espo/Hooks/Common/Stream.php b/application/Espo/Hooks/Common/Stream.php index f949559209..dd1f13a66d 100644 --- a/application/Espo/Hooks/Common/Stream.php +++ b/application/Espo/Hooks/Common/Stream.php @@ -82,7 +82,7 @@ class Stream extends \Espo\Core\Hooks\Base protected function getStreamService() { if (empty($this->streamService)) { - $this->streamService = $this->getServiceFactory()->createByClassName('\\Espo\\Services\\Stream'); + $this->streamService = $this->getServiceFactory()->create('Stream'); } return $this->streamService; } diff --git a/application/Espo/Modules/Crm/Controllers/InboundEmail.php b/application/Espo/Modules/Crm/Controllers/InboundEmail.php index 05a51e1885..c6966681d0 100644 --- a/application/Espo/Modules/Crm/Controllers/InboundEmail.php +++ b/application/Espo/Modules/Crm/Controllers/InboundEmail.php @@ -5,4 +5,16 @@ namespace Espo\Modules\Crm\Controllers; class InboundEmail extends \Espo\Core\Controllers\Record { + public function actionFetch($params, $data, $request) + { + $id = $request->get('id'); + try { + $this->getRecordService()->fetchFromMailServer($id); + } catch (\Exception $e) { + echo $e->getMessage(); + } + echo "--"; + die; + } + } diff --git a/application/Espo/Modules/Crm/Resources/layouts/InboundEmail/detail.json b/application/Espo/Modules/Crm/Resources/layouts/InboundEmail/detail.json index 67e85e09a3..6d44a66a34 100644 --- a/application/Espo/Modules/Crm/Resources/layouts/InboundEmail/detail.json +++ b/application/Espo/Modules/Crm/Resources/layouts/InboundEmail/detail.json @@ -12,12 +12,17 @@ "label":"IMAP", "rows":[ [ - {"name":"serverAddress"},{"name":"username"} + {"name":"host"},{"name":"username"} ], [ - {"name":"serverPort"},{"name":"password"}], - [{"name":"monitoredFolders"}], - [{"name":"trashFolder"},false] + {"name":"port"},{"name":"password"} + ], + [ + {"name":"monitoredFolders"} + ], + [ + {"name":"trashFolder"},false + ] ] }, { diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/InboundEmail.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/InboundEmail.json index 9650b75562..97141b3e80 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/InboundEmail.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/InboundEmail.json @@ -8,11 +8,11 @@ "type": "enum", "options": ["Active", "Inactive"] }, - "serverAddress": { + "host": { "type": "varchar", "required": true }, - "serverPort": { + "port": { "type": "varchar", "default": "143", "required": true @@ -31,7 +31,6 @@ }, "trashFolder": { "type": "varchar", - "required": true, "default": "TRASH" }, "teams": { diff --git a/application/Espo/Modules/Crm/Services/Activities.php b/application/Espo/Modules/Crm/Services/Activities.php index 3ad9fd58f7..8685a41320 100644 --- a/application/Espo/Modules/Crm/Services/Activities.php +++ b/application/Espo/Modules/Crm/Services/Activities.php @@ -49,7 +49,7 @@ class Activities extends \Espo\Core\Services\Base $qu = " SELECT meeting.id AS 'id', meeting.name AS 'name', meeting.date_start AS 'dateStart', meeting.date_end AS 'dateEnd', 'Meeting' AS '_scope', meeting.assigned_user_id AS assignedUserId, TRIM(CONCAT(user.first_name, ' ', user.last_name)) AS assignedUserName, - meeting.parent_type AS 'parentType', meeting.parent_id AS 'parentId', meeting.status AS status + meeting.parent_type AS 'parentType', meeting.parent_id AS 'parentId', meeting.status AS status, meeting.created_at AS createdAt FROM `meeting` LEFT JOIN `user` ON user.id = meeting.assigned_user_id "; @@ -94,7 +94,7 @@ class Activities extends \Espo\Core\Services\Base $qu = " SELECT meeting.id AS 'id', meeting.name AS 'name', meeting.date_start AS 'dateStart', meeting.date_end AS 'dateEnd', 'Meeting' AS '_scope', meeting.assigned_user_id AS assignedUserId, TRIM(CONCAT(user.first_name, ' ', user.last_name)) AS assignedUserName, - meeting.parent_type AS 'parentType', meeting.parent_id AS 'parentId', meeting.status AS status + meeting.parent_type AS 'parentType', meeting.parent_id AS 'parentId', meeting.status AS status, meeting.created_at AS createdAt FROM `meeting` LEFT JOIN `user` ON user.id = meeting.assigned_user_id JOIN contact_meeting ON @@ -123,7 +123,7 @@ class Activities extends \Espo\Core\Services\Base $qu = " SELECT call.id AS 'id', call.name AS 'name', call.date_start AS 'dateStart', call.date_end AS 'dateEnd', 'Call' AS '_scope', call.assigned_user_id AS assignedUserId, TRIM(CONCAT(user.first_name, ' ', user.last_name)) AS assignedUserName, - call.parent_type AS 'parentType', call.parent_id AS 'parentId', call.status AS status + call.parent_type AS 'parentType', call.parent_id AS 'parentId', call.status AS status, call.created_at AS createdAt FROM `call` LEFT JOIN `user` ON user.id = call.assigned_user_id "; @@ -168,7 +168,7 @@ class Activities extends \Espo\Core\Services\Base $qu = " SELECT call.id AS 'id', call.name AS 'name', call.date_start AS 'dateStart', call.date_end AS 'dateEnd', 'Call' AS '_scope', call.assigned_user_id AS assignedUserId, TRIM(CONCAT(user.first_name, ' ', user.last_name)) AS assignedUserName, - call.parent_type AS 'parentType', call.parent_id AS 'parentId', call.status AS status + call.parent_type AS 'parentType', call.parent_id AS 'parentId', call.status AS status, call.created_at AS createdAt FROM `call` LEFT JOIN `user` ON user.id = call.assigned_user_id JOIN call_contact ON @@ -198,21 +198,26 @@ class Activities extends \Espo\Core\Services\Base SELECT DISTINCT email.id AS 'id', email.name AS 'name', email.date_sent AS 'dateStart', '' AS 'dateEnd', 'Email' AS '_scope', email.assigned_user_id AS assignedUserId, TRIM(CONCAT(user.first_name, ' ', user.last_name)) AS assignedUserName, - email.parent_type AS 'parentType', email.parent_id AS 'parentId', email.status AS status + email.parent_type AS 'parentType', email.parent_id AS 'parentId', email.status AS status, email.created_at AS createdAt FROM `email` LEFT JOIN `user` ON user.id = email.assigned_user_id "; if ($this->isPerson($scope)) { $qu .= " - JOIN email_email_address ON + LEFT JOIN entity_email_address AS entity_email_address_2 ON + entity_email_address_2.email_address_id = email.from_email_address_id AND + entity_email_address_2.entity_type = " . $this->getPDO()->quote($scope) . " AND + entity_email_address_2.deleted = 0 + + LEFT JOIN email_email_address ON email_email_address.email_id = email.id AND email_email_address.deleted = 0 - JOIN entity_email_address ON - entity_email_address.email_address_id = email_email_address.email_address_id AND - entity_email_address.entity_id = ".$this->getPDO()->quote($id)." AND - entity_email_address.entity_type = ".$this->getPDO()->quote($scope)." AND - entity_email_address.deleted = 0 + LEFT JOIN entity_email_address AS entity_email_address_1 ON + entity_email_address_1.email_address_id = email_email_address.email_address_id AND + + entity_email_address_1.entity_type = " . $this->getPDO()->quote($scope) . " AND + entity_email_address_1.deleted = 0 "; } @@ -225,6 +230,10 @@ class Activities extends \Espo\Core\Services\Base $qu .= " AND email.parent_type = ".$this->getPDO()->quote($scope)." AND email.parent_id = ".$this->getPDO()->quote($id)." "; + } else { + $qu .= " + AND (entity_email_address_1.entity_id = ".$this->getPDO()->quote($id)." OR entity_email_address_2.entity_id = ".$this->getPDO()->quote($id).") + "; } if (!empty($notIn)) { @@ -260,7 +269,7 @@ class Activities extends \Espo\Core\Services\Base $totalCount = $row['count']; $qu .= " - ORDER BY dateStart DESC + ORDER BY dateStart DESC, createdAt DESC "; if (!empty($params['maxSize'])) { diff --git a/application/Espo/Modules/Crm/Services/InboundEmail.php b/application/Espo/Modules/Crm/Services/InboundEmail.php new file mode 100644 index 0000000000..4e312a0838 --- /dev/null +++ b/application/Espo/Modules/Crm/Services/InboundEmail.php @@ -0,0 +1,241 @@ +dependencies[] = 'fileManager'; + } + + protected function getFileManager() + { + return $this->injections['fileManager']; + } + + protected function findFolder($storage, $path) + { + $arr = explode('/', $path); + $pointer = $storage->getFolders(); + foreach ($arr as $folderName) { + $pointer = $pointer->$folderName; + } + return $pointer; + } + + // TODO try catch this in cron + public function fetchFromMailServer($id) + { + $inboundEmail = $this->getEntity($id); + $inboundEmail->loadLinkMultipleField('teams'); + + if ($inboundEmail->get('status') != 'Active') { + throw new Error(); + } + + $storage = new \Zend\Mail\Storage\Imap(array( + 'host' => $inboundEmail->get('host'), + 'port' => $inboundEmail->get('port'), + 'user' => $inboundEmail->get('username'), + 'password' => $inboundEmail->get('password'), + )); + + if (empty($storage)) { + throw new Error("Could not connect to IMAP of Inbound Email {$inboundEmail->id}."); + } + + $trash = null; + $trashFolder = $inboundEmail->get('trashFolder'); + if ($trashFolder) { + $trash = $this->findFolder($storage, $trashFolder); + } + + $monitoredFolders = $inboundEmail->get('monitoredFolders'); + if (empty($monitoredFolders)) { + $monitoredFolders = 'INBOX'; + } + + $monitoredFoldersArr = explode(',', $monitoredFolders); + foreach ($monitoredFoldersArr as $path) { + $toRemove = array(); + $path = trim($path); + + $folder = $this->findFolder($storage, $path); + $storage->selectFolder($folder); + + foreach ($storage as $number => $message) { + $this->importMessage($inboundEmail, $message); + } + + while ($storage->countMessages()) { + if ($trash) { + $folder->moveMessage(1, $trash); + } else { + $storage->removeMessage(1); + } + } + + } + } + + protected function getAddressListFromMessage($message, $type) + { + $addressList = array(); + if (isset($message->$type)) { + + $list = $message->getHeader($type)->getAddressList(); + foreach ($list as $address) { + $addressList[] = $address->getEmail(); + } + } + return $addressList; + } + + protected function importMessage($inboundEmail, $message) + { + $result = false; + + try { + $email = $this->getEntityManager()->getEntity('Email'); + $email->set('teamsIds', $inboundEmail->get('teamsIds')); + $email->set('isHtml', false); + $email->set('name', $message->subject); + $email->set('attachmentsIds', array()); + + $email->set('assignedUserId', $this->getUser()->id); + + $fromArr = $this->getAddressListFromMessage($message, 'from'); + $email->set('from', $fromArr[0]); + $email->set('to', implode(';', $this->getAddressListFromMessage($message, 'to'))); + $email->set('cc', implode(';', $this->getAddressListFromMessage($message, 'cc'))); + $email->set('bcc', implode(';', $this->getAddressListFromMessage($message, 'bcc'))); + + $email->set('status', 'Archived'); + + $dt = new \DateTime($message->date); + if ($dt) { + $dateSent = $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); + $email->set('dateSent', $dateSent); + } + + if ($message->isMultipart()) { + foreach (new \RecursiveIteratorIterator($message) as $part) { + $this->importPartDataToEmail($email, $part); + } + + } else { + $this->importPartDataToEmail($email, $message); + } + + $this->getEntityManager()->saveEntity($email); + echo $email->id ."
"; + + + if ($inboundEmail->get('createCase')) { + // TODO check case exists + + if (preg_match('/\[#([0-9]+)[^0-9]*\]/', $email->get('name'), $m)) { + $caseNumber = $m[1]; + $case = $this->getEntityManager()->getRepository('Case')->where(array( + 'number' => $caseNumber + ))->findOne(); + if ($case) { + $email->set('parentType', 'Case'); + $email->set('parentId', $case->id); + $this->getEntityManager()->saveEntity($email); + $this->getServiceFactory()->create('Stream')->noteEmail($case, $email); + } + } else { + $case = $this->emailToCase($email, $inboundEmail->get('caseDistribution')); + // TODO auto-reply + } + } else { + // TODO auto-reply + } + + $result = true; + + } catch (\Exception $e){ + // TODO log + + } + + return $result; + } + + protected function emailToCase(\Espo\Entities\Email $email, $caseDistribution = 'Round-Robin') + { + $case = $this->getEntityManager()->getEntity('Case'); + $case->populateDefaults(); + $case->set('name', $email->get('name')); + $case->set('description', $email->get('bodyPlain')); + $case->set('teamsIds', $email->get('teamsIds')); + + // TODO distribution + $case->set('assignedUserId', $this->getUser()->id); + + + $contact = $this->getEntityManager()->getRepository('Contact')->where(array( + 'EmailAddress.id' => $email->get('fromEmailAddressId') + ))->findOne(); + if ($contact) { + $case->set('contactId', $contact->id); + if ($contact->get('accountId')) { + $case->set('accountId', $contact->get('accountId')); + } + } + + $this->getEntityManager()->saveEntity($case); + + $email->set('parentType', 'Case'); + $email->set('parentId', $case->id); + $this->getEntityManager()->saveEntity($email); + + return $case; + } + + protected function importPartDataToEmail(\Espo\Entities\Email $email, $part) + { + try { + $type = strtok($part->contentType, ';'); + switch ($type) { + case 'text/plain': + if (!$email->get('body')) { + $email->set('body', $part->getContent()); + } + $email->set('bodyPlain', $part->getContent()); + break; + case 'text/html': + $email->set('body', $part->getContent()); + $email->set('isHtml', true); + break; + default: + if (isset($part->ContentDisposition)) { + if (preg_match('/filename="?([^"]+)"?/i', $part->ContentDisposition, $m)) { + $fileName = $m[1]; + $attachment = $this->getEntityManager()->getEntity('Attachment'); + $attachment->set('name', $fileName); + $attachment->set('type', $type); + + $this->getEntityManager()->saveEntity($attachment); + + $path = 'data/upload/' . $attachment->id; + $content = base64_decode($part->getContent()); + $this->getFileManager()->setContent($content, $path); + $attachmentsIds = $email->get('attachmentsIds'); + $attachmentsIds[] = $attachment->id; + $email->set('attachmentsIds', $attachmentsIds); + } + } + } + } catch (\Exception $e){ + // TODO log + } + } +} + diff --git a/application/Espo/ORM/Entity.php b/application/Espo/ORM/Entity.php index 3aff942ac9..8e969cb0f9 100644 --- a/application/Espo/ORM/Entity.php +++ b/application/Espo/ORM/Entity.php @@ -257,5 +257,14 @@ abstract class Entity implements IEntity $this->isFetched = true; $this->fetchedValuesContainer = $this->valuesContainer; } + + public function populateDefaults() + { + foreach ($this->fields as $field => $defs) { + if (array_key_exists('default', $defs)) { + $this->valuesContainer[$field] = $defs['default']; + } + } + } } diff --git a/application/Espo/ORM/Repositories/RDB.php b/application/Espo/ORM/Repositories/RDB.php index e62de7ee47..1457a8e86d 100644 --- a/application/Espo/ORM/Repositories/RDB.php +++ b/application/Espo/ORM/Repositories/RDB.php @@ -244,11 +244,11 @@ class RDB extends \Espo\ORM\Repository if (empty($this->listParams['joins'])) { $this->listParams['joins'] = array(); } - + foreach ($args as &$param) { if (is_array($param)) { foreach ($param as $k => $v) { - $this->listParams['joins'][] = array(); + $this->listParams['joins'][] = $v; } } else { $this->listParams['joins'][] = $param; diff --git a/application/Espo/Resources/metadata/entityDefs/Email.json b/application/Espo/Resources/metadata/entityDefs/Email.json index affbe26572..600d75b351 100644 --- a/application/Espo/Resources/metadata/entityDefs/Email.json +++ b/application/Espo/Resources/metadata/entityDefs/Email.json @@ -4,6 +4,11 @@ "type": "varchar", "required": true }, + "subject": { + "type": "varchar", + "required": true, + "notStorable": true + }, "from": { "type": "varchar", "db": false, @@ -22,6 +27,10 @@ "type": "varchar", "db": false }, + "bodyPlain": { + "type": "text", + "readOnly": true + }, "body": { "type": "text", "view": "Fields.Wysiwyg" diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index 1f90008b8f..c88310f9c7 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -130,14 +130,17 @@ class Record extends \Espo\Core\Services\Base protected function getSelectManager($entityName) { - $moduleName = $this->getMetadata()->getScopeModuleName($entityName); - if ($moduleName) { - $className = '\\Espo\\Modules\\' . $moduleName . '\\SelectManagers\\' . Util::normilizeClassName($entityName); - } else { - $className = '\\Espo\\SelectManagers\\' . Util::normilizeClassName($entityName); - } - if (!class_exists($className)) { - $className = '\\Espo\\Core\\SelectManager'; + $className = '\\Espo\\Custom\\SelectManagers\\' . Util::normilizeClassName($entityName); + if (!class_exists($className)) { + $moduleName = $this->getMetadata()->getScopeModuleName($entityName); + if ($moduleName) { + $className = '\\Espo\\Modules\\' . $moduleName . '\\SelectManagers\\' . Util::normilizeClassName($entityName); + } else { + $className = '\\Espo\\SelectManagers\\' . Util::normilizeClassName($entityName); + } + if (!class_exists($className)) { + $className = '\\Espo\\Core\\SelectManager'; + } } $selectManager = new $className($this->getEntityManager(), $this->getUser(), $this->getAcl(), $this->getMetadata()); diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index 7ae8264197..775146059a 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -237,6 +237,26 @@ class Stream extends \Espo\Core\Services\Base } } + public function noteEmail(Entity $entity, Entity $email) + { + $entityName = $entity->getEntityName(); + + $note = $this->getEntityManager()->getEntity('Note'); + + $note->set('type', 'Email'); + $note->set('parentId', $entity->id); + $note->set('parentType', $entityName); + + $data = array(); + + $data['emailId'] = $email->id; + $data['emailName'] = $email->get('name'); + + $note->set('data', json_encode($data)); + + $this->getEntityManager()->saveEntity($note); + } + public function noteCreate(Entity $entity) { $entityName = $entity->getEntityName();