From e1579295da012f27e64d1864b962c06d452bc76d Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Wed, 27 Aug 2014 17:01:31 +0300 Subject: [PATCH] email account --- application/Espo/Core/Mail/Importer.php | 222 ++++++++++++++ application/Espo/Core/Mail/Storage/Imap.php | 13 + .../Modules/Crm/Services/InboundEmail.php | 282 +++--------------- .../metadata/entityDefs/EmailAccount.json | 3 + application/Espo/Services/EmailAccount.php | 70 ++++- composer.json | 3 +- composer.lock | 18 +- vendor/autoload.php | 2 +- vendor/composer/autoload_real.php | 8 +- 9 files changed, 369 insertions(+), 252 deletions(-) create mode 100644 application/Espo/Core/Mail/Importer.php create mode 100644 application/Espo/Core/Mail/Storage/Imap.php diff --git a/application/Espo/Core/Mail/Importer.php b/application/Espo/Core/Mail/Importer.php new file mode 100644 index 0000000000..b52aeff073 --- /dev/null +++ b/application/Espo/Core/Mail/Importer.php @@ -0,0 +1,222 @@ +entityManager = $entityManager; + } + + protected function getEntityManager() + { + return $this->entityManager; + } + + protected function importMessage($message, $userId, $teamsIds = array()) + { + try { + $email = $this->getEntityManager()->getEntity('Email'); + + $email->set('isHtml', false); + $email->set('name', $message->subject); + $email->set('status', 'Archived'); + $email->set('attachmentsIds', array()); + $email->set('assignedUserId', $userId); + $email->set('teamsIds', $teamsIds); + + $fromArr = $this->getAddressListFromMessage($message, 'from'); + if (isset($message->from)) { + $email->set('fromName', $message->from); + } + $email->set('from', $fromArr[0]); + $email->set('to', implode(';', $this->getAddressListFromMessage($message, 'to'))); + $email->set('cc', implode(';', $this->getAddressListFromMessage($message, 'cc'))); + + if (isset($message->messageId) && !empty($message->messageId)) { + $email->set('messageId', $message->messageId); + if (isset($message->deliveredTo)) { + $email->set('messageIdInternal', $message->messageId . '-' . $message->deliveredTo); + } + } + + if ($this->checkIsDuplicate($email)) { + return false; + } + + $dt = new \DateTime($message->date); + if ($dt) { + $dateSent = $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); + $email->set('dateSent', $dateSent); + } + + $inlineIds = array(); + + if ($message->isMultipart()) { + foreach (new \RecursiveIteratorIterator($message) as $part) { + $this->importPartDataToEmail($email, $part, $inlineIds); + } + } else { + $this->importPartDataToEmail($email, $message, $inlineIds); + } + + $body = $email->get('body'); + if (!empty($body)) { + foreach ($inlineIds as $cid => $attachmentId) { + $body = str_replace('cid:' . $cid, '?entryPoint=attachment&id=' . $attachmentId, $body); + } + $email->set('body', $body); + } + + $this->getEntityManager()->saveEntity($email); + + return $email; + + } catch (\Exception $e) {} + } + + protected function checkIsDuplicate($email) + { + if ($email->has('messageIdInternal')) { + $duplicate = $this->getEntityManager()->getRepository('Email')->where(array( + 'messageIdInternal' => $email->has('messageIdInternal') + ))->findOne(); + if ($duplicate) { + return true; + } + } + } + + 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 importPartDataToEmail(\Espo\Entities\Email $email, $part, &$inlineIds = array()) + { + try { + $type = strtok($part->contentType, ';'); + $encoding = null; + + switch ($type) { + case 'text/plain': + $content = $this->getContentFromPart($part); + if (!$email->get('body')) { + $email->set('body', $content); + } + $email->set('bodyPlain', $content); + break; + case 'text/html': + $content = $this->getContentFromPart($part); + $email->set('body', $content); + $email->set('isHtml', true); + break; + default: + $content = $part->getContent(); + $disposition = null; + + $fileName = null; + $contentId = null; + + if (isset($part->ContentDisposition)) { + if (strpos($part->ContentDisposition, 'attachment') === 0) { + if (preg_match('/filename="?([^"]+)"?/i', $part->ContentDisposition, $m)) { + $fileName = $m[1]; + $disposition = 'attachment'; + } + } else if (strpos($part->ContentDisposition, 'inline') === 0) { + $contentId = trim($part->contentID, '<>'); + $fileName = $contentId; + $disposition = 'inline'; + } + } + + if (isset($part->contentTransferEncoding)) { + $encoding = strtolower($part->getHeader('Content-Transfer-Encoding')->getTransferEncoding()); + } + + $attachment = $this->getEntityManager()->getEntity('Attachment'); + $attachment->set('name', $fileName); + $attachment->set('type', $type); + $attachment->set('role', 'Inline Attachment'); + + $this->getEntityManager()->saveEntity($attachment); + + $path = 'data/upload/' . $attachment->id; + + if ($encoding == 'base64') { + $content = base64_decode($content); + } + $this->getFileManager()->putContents($path, $content); + + if ($disposition == 'attachment') { + $attachmentsIds = $email->get('attachmentsIds'); + $attachmentsIds[] = $attachment->id; + $email->set('attachmentsIds', $attachmentsIds); + } else if ($disposition == 'inline') { + $inlineIds[$contentId] = $attachment->id; + } + } + } catch (\Exception $e){ + // TODO log + } + } + + protected function getContentFromPart($part) + { + if ($part instanceof \Zend\Mime\Part) { + $content = $part->getRawContent(); + if (strtolower($part->charset) != 'utf-8') { + $content = mb_convert_encoding($content, 'UTF-8', $part->charset); + } + } else { + $content = $part->getContent(); + + $encoding = null; + + if (isset($part->contentTransferEncoding)) { + $cteHeader = $part->getHeader('Content-Transfer-Encoding'); + $encoding = strtolower($cteHeader->getTransferEncoding()); + } + + if ($encoding == 'base64') { + $content = base64_decode($content); + } + + $charset = 'UTF-8'; + + if (isset($part->contentType)) { + $ctHeader = $part->getHeader('Content-Type'); + $charsetParamValue = $ctHeader->getParameter('charset'); + if (!empty($charsetParamValue)) { + $charset = strtoupper($charsetParamValue); + } + } + + if ($charset !== 'UTF-8') { + $content = mb_convert_encoding($content, 'UTF-8', $charset); + } + + if (isset($part->contentTransferEncoding)) { + $cteHeader = $part->getHeader('Content-Transfer-Encoding'); + if ($cteHeader->getTransferEncoding() == 'quoted-printable') { + $content = quoted_printable_decode($content); + } + } + } + return $content; + } +} diff --git a/application/Espo/Core/Mail/Storage/Imap.php b/application/Espo/Core/Mail/Storage/Imap.php new file mode 100644 index 0000000000..d74062715e --- /dev/null +++ b/application/Espo/Core/Mail/Storage/Imap.php @@ -0,0 +1,13 @@ +protocol->search(array('UID ' . $uid . ':*')); + } +} + diff --git a/application/Espo/Modules/Crm/Services/InboundEmail.php b/application/Espo/Modules/Crm/Services/InboundEmail.php index 0b0967692b..69a625a3c2 100644 --- a/application/Espo/Modules/Crm/Services/InboundEmail.php +++ b/application/Espo/Modules/Crm/Services/InboundEmail.php @@ -27,8 +27,6 @@ use \Espo\ORM\Entity; use \Espo\Core\Exceptions\Error; use \Espo\Core\Exceptions\Forbidden; -use \Zend\Mime\Mime as Mime; - class InboundEmail extends \Espo\Services\Record { protected $internalFields = array('password'); @@ -58,7 +56,6 @@ class InboundEmail extends \Espo\Services\Record return $result; } - protected function init() { $this->dependencies[] = 'fileManager'; @@ -119,12 +116,19 @@ class InboundEmail extends \Espo\Services\Record } public function fetchFromMailServer(Entity $inboundEmail) - { - + { if ($inboundEmail->get('status') != 'Active') { throw new Error(); } + $importer = \Espo\Core\Mail\Importer($this->getEntityManager()); + + $teamId = $inboundEmail->get('teamId'); + $userId = $this->getUser()->id; + if ($inboundEmail->get('assignToUserId')) { + $userId = $inboundEmail->get('assignToUserId'); + } + $imapParams = array( 'host' => $inboundEmail->get('host'), 'port' => $inboundEmail->get('port'), @@ -160,10 +164,21 @@ class InboundEmail extends \Espo\Services\Record $path = trim($path); $folder = $this->findFolder($storage, $path); - $storage->selectFolder($folder); + $storage->selectFolder($folder); foreach ($storage as $number => $message) { - $this->importMessage($inboundEmail, $message); + $email = $this->importMessage($message, $userId, array($teamId)); + + if ($email) { + if ($inboundEmail->get('createCase')) { + $this->createCase($inboundEmail, $email); + } else { + if ($inboundEmail->get('reply')) { + $user = $this->getEntityManager()->getEntity('User', $userId); + $this->autoReply($inboundEmail, $email, $user); + } + } + } } while ($storage->countMessages()) { @@ -174,116 +189,31 @@ class InboundEmail extends \Espo\Services\Record } } - protected function getAddressListFromMessage($message, $type) + protected function createCase($inboundEmail, $email) { - $addressList = array(); - if (isset($message->$type)) { - - $list = $message->getHeader($type)->getAddressList(); - foreach ($list as $address) { - $addressList[] = $address->getEmail(); + 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')->noteEmailReceived($case, $email); + } + } else { + $params = array( + 'caseDistribution' => $inboundEmail->get('caseDistribution'), + 'teamId' => $inboundEmail->get('teamId'), + 'userId' => $inboundEmail->get('assignToUserId'), + ); + $case = $this->emailToCase($email, $params); + $user = $this->getEntityManager()->getEntity('User', $case->get('assignedUserId')); + if ($inboundEmail->get('reply')) { + $this->autoReply($inboundEmail, $email, $case, $user); } } - return $addressList; - } - - protected function importMessage($inboundEmail, $message) - { - $result = false; - - try { - $email = $this->getEntityManager()->getEntity('Email'); - if ($inboundEmail->get('teamId')) { - $email->set('teamsIds', array($inboundEmail->get('teamId'))); - } - - $email->set('isHtml', false); - $email->set('name', $message->subject); - $email->set('attachmentsIds', array()); - - $userId = $this->getUser()->id; - if ($inboundEmail->get('assignToUserId')) { - $userId = $inboundEmail->get('assignToUserId'); - } - $email->set('assignedUserId', $userId); - - $fromArr = $this->getAddressListFromMessage($message, 'from'); - - if (isset($message->from)) { - $email->set('fromName', $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); - } - - $inlineIds = array(); - - if ($message->isMultipart()) { - foreach (new \RecursiveIteratorIterator($message) as $part) { - $this->importPartDataToEmail($email, $part, $inlineIds); - } - } else { - $this->importPartDataToEmail($email, $message, $inlineIds); - } - - $body = $email->get('body'); - if (!empty($body)) { - foreach ($inlineIds as $cid => $attachmentId) { - $body = str_replace('cid:' . $cid, '?entryPoint=attachment&id=' . $attachmentId, $body); - } - $email->set('body', $body); - } - - $this->getEntityManager()->saveEntity($email); - - if ($inboundEmail->get('createCase')) { - 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')->noteEmailReceived($case, $email); - } - } else { - $params = array( - 'caseDistribution' => $inboundEmail->get('caseDistribution'), - 'teamId' => $inboundEmail->get('teamId'), - 'userId' => $inboundEmail->get('assignToUserId'), - ); - $case = $this->emailToCase($email, $params); - $user = $this->getEntityManager()->getEntity('User', $case->get('assignedUserId')); - if ($inboundEmail->get('reply')) { - $this->autoReply($inboundEmail, $email, $case, $user); - } - } - } else { - if ($inboundEmail->get('reply')) { - $user = $this->getEntityManager()->getEntity('User', $userId); - $this->autoReply($inboundEmail, $email, $user); - } - } - - $result = true; - - } catch (\Exception $e){ - // TODO log - } - - return $result; } protected function assignRoundRobin($case, $team) @@ -369,124 +299,10 @@ class InboundEmail extends \Espo\Services\Record $this->getEntityManager()->saveEntity($email); $case = $this->getEntityManager()->getEntity('Case', $case->id); + return $case; } - - protected function getContentFromPart($part) - { - if ($part instanceof \Zend\Mime\Part) { - $content = $part->getRawContent(); - if (strtolower($part->charset) != 'utf-8') { - $content = mb_convert_encoding($content, 'UTF-8', $part->charset); - } - } else { - $content = $part->getContent(); - - $encoding = null; - - if (isset($part->contentTransferEncoding)) { - $cteHeader = $part->getHeader('Content-Transfer-Encoding'); - $encoding = strtolower($cteHeader->getTransferEncoding()); - } - - if ($encoding == 'base64') { - $content = base64_decode($content); - } - - $charset = 'UTF-8'; - - if (isset($part->contentType)) { - $ctHeader = $part->getHeader('Content-Type'); - $charsetParamValue = $ctHeader->getParameter('charset'); - if (!empty($charsetParamValue)) { - $charset = strtoupper($charsetParamValue); - } - } - - if ($charset !== 'UTF-8') { - $content = mb_convert_encoding($content, 'UTF-8', $charset); - } - - if (isset($part->contentTransferEncoding)) { - $cteHeader = $part->getHeader('Content-Transfer-Encoding'); - if ($cteHeader->getTransferEncoding() == 'quoted-printable') { - $content = quoted_printable_decode($content); - } - } - } - return $content; - } - - protected function importPartDataToEmail(\Espo\Entities\Email $email, $part, &$inlineIds = array()) - { - try { - $type = strtok($part->contentType, ';'); - $encoding = null; - - switch ($type) { - case 'text/plain': - $content = $this->getContentFromPart($part); - if (!$email->get('body')) { - $email->set('body', $content); - } - $email->set('bodyPlain', $content); - break; - case 'text/html': - $content = $this->getContentFromPart($part); - $email->set('body', $content); - $email->set('isHtml', true); - break; - default: - $content = $part->getContent(); - $disposition = null; - - $fileName = null; - $contentId = null; - - if (isset($part->ContentDisposition)) { - if (strpos($part->ContentDisposition, 'attachment') === 0) { - if (preg_match('/filename="?([^"]+)"?/i', $part->ContentDisposition, $m)) { - $fileName = $m[1]; - $disposition = 'attachment'; - } - } else if (strpos($part->ContentDisposition, 'inline') === 0) { - $contentId = trim($part->contentID, '<>'); - $fileName = $contentId; - $disposition = 'inline'; - } - } - - if (isset($part->contentTransferEncoding)) { - $encoding = strtolower($part->getHeader('Content-Transfer-Encoding')->getTransferEncoding()); - } - - $attachment = $this->getEntityManager()->getEntity('Attachment'); - $attachment->set('name', $fileName); - $attachment->set('type', $type); - $attachment->set('role', 'Inline Attachment'); - - $this->getEntityManager()->saveEntity($attachment); - - $path = 'data/upload/' . $attachment->id; - - if ($encoding == 'base64') { - $content = base64_decode($content); - } - $this->getFileManager()->putContents($path, $content); - - if ($disposition == 'attachment') { - $attachmentsIds = $email->get('attachmentsIds'); - $attachmentsIds[] = $attachment->id; - $email->set('attachmentsIds', $attachmentsIds); - } else if ($disposition == 'inline') { - $inlineIds[$contentId] = $attachment->id; - } - } - } catch (\Exception $e){ - // TODO log - } - } - + protected function autoReply($inboundEmail, $email, $case = null, $user = null) { try { @@ -548,11 +364,11 @@ class InboundEmail extends \Espo\Services\Record } $this->getEntityManager()->removeEntity($reply); + + return true; } - } catch (\Exception $e){ - // TODO log - } + } catch (\Exception $e) {} } } diff --git a/application/Espo/Resources/metadata/entityDefs/EmailAccount.json b/application/Espo/Resources/metadata/entityDefs/EmailAccount.json index 18d55ae702..2b6509724c 100644 --- a/application/Espo/Resources/metadata/entityDefs/EmailAccount.json +++ b/application/Espo/Resources/metadata/entityDefs/EmailAccount.json @@ -33,6 +33,9 @@ "default": "INBOX", "view": "EmailAccount.Fields.Folders" }, + "fetchData": { + "type": "text" + }, "createdAt": { "type": "datetime", "readOnly": true diff --git a/application/Espo/Services/EmailAccount.php b/application/Espo/Services/EmailAccount.php index d9cf6e62a9..18f68f9f1c 100644 --- a/application/Espo/Services/EmailAccount.php +++ b/application/Espo/Services/EmailAccount.php @@ -27,15 +27,11 @@ use \Espo\ORM\Entity; use \Espo\Core\Exceptions\Error; use \Espo\Core\Exceptions\Forbidden; - -use \Zend\Mime\Mime as Mime; - class EmailAccount extends Record -{ - +{ protected $internalFields = array('password'); - protected $readOnlyFields = array('assignedUserId'); + protected $readOnlyFields = array('assignedUserId', 'fetchData'); public function getFolders($params) { @@ -71,11 +67,30 @@ class EmailAccount extends Record } public function fetchFromMailServer(Entity $emailAccount) - { + { if ($emailAccount->get('status') != 'Active') { throw new Error(); } + $importer = \Espo\Core\Mail\Importer($this->getEntityManager()); + + $user = $this->getEntityManager()->getEntity('User', $emailAccount->get('assignedUserId')); + + if (!$user) { + throw new Error(); + } + + $userId = $user->id; + $teamId = $user->get('defaultTeam'); + + $fetchData = json_decode($emailAccount->get('fetchData'), true); + if (empty($fetchData)) { + $fetchData = array(); + } + if (!array_key_exists('lastUIDs', $fetchData)) { + $fetchData['lastUIDs'] = array(); + } + $imapParams = array( 'host' => $emailAccount->get('host'), 'port' => $emailAccount->get('port'), @@ -87,7 +102,48 @@ class EmailAccount extends Record $imapParams['ssl'] = 'SSL'; } + $storage = new \Espo\Core\Mail\Storage\Imap($imapParams); + + $monitoredFolders = $emailAccount->get('monitoredFolders'); + if (empty($monitoredFolders)) { + throw new Error(); + } + + $monitoredFoldersArr = explode(',', $monitoredFolders); + foreach ($monitoredFoldersArr as $folder) { + $folder = trim($folder); + + $storage->selectFolder($folder); + + $lastUID = 0; + if (!empty($fetchData['lastUIDs'][$folder])) { + $lastUID = $fetchData['lastUIDs'][$folder]; + } + + $ids = $storage->getIdsFromUID(); + + print_r($ids); + + foreach ($ids as $k => $id) { + $message = $storage->getMessage($id); + + $importer->importMessage($message, $userId, array($teamId)); + + if ($k == count($ids) - 1) { + $lastUID = $storage->getUniqueId($id); + } + } + + $fetchData['lastUIDs'][$folder] = $lastUID; + + print_r($fetchData); + + } } + + + + } diff --git a/composer.json b/composer.json index 4ad2a61ae6..175bc32491 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,7 @@ "zendframework/zend-validator": "2.*", "zendframework/zend-mail": "2.*", "zendframework/zend-ldap": "2.*", - "monolog/monolog": "1.*", - "tedivm/fetch": "0.5.*" + "monolog/monolog": "1.*" }, "autoload": { "psr-0": { diff --git a/composer.lock b/composer.lock index 216ce41ec5..c74a3ec4c3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "47bc2d2cb4a245965c961a08febd5761", + "hash": "2823824401429f765521f2d5c60231d7", "packages": [ { "name": "doctrine/annotations", @@ -1490,9 +1490,17 @@ "time": "2014-01-07 13:28:54" } ], - "aliases": [], + "aliases": [ + + ], "minimum-stability": "stable", - "stability-flags": [], - "platform": [], - "platform-dev": [] + "stability-flags": [ + + ], + "platform": [ + + ], + "platform-dev": [ + + ] } diff --git a/vendor/autoload.php b/vendor/autoload.php index a8b11392c9..58e1a1bad2 100644 --- a/vendor/autoload.php +++ b/vendor/autoload.php @@ -4,4 +4,4 @@ require_once __DIR__ . '/composer' . '/autoload_real.php'; -return ComposerAutoloaderInit788374cf70ca1db1fb5ad2e33550a92a::getLoader(); +return ComposerAutoloaderInit89af41ceda5657caa52b70dbf0e455f1::getLoader(); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 5518c587f0..bd655a87f4 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit788374cf70ca1db1fb5ad2e33550a92a +class ComposerAutoloaderInit89af41ceda5657caa52b70dbf0e455f1 { private static $loader; @@ -19,9 +19,9 @@ class ComposerAutoloaderInit788374cf70ca1db1fb5ad2e33550a92a return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInit788374cf70ca1db1fb5ad2e33550a92a', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInit89af41ceda5657caa52b70dbf0e455f1', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); - spl_autoload_unregister(array('ComposerAutoloaderInit788374cf70ca1db1fb5ad2e33550a92a', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInit89af41ceda5657caa52b70dbf0e455f1', 'loadClassLoader')); $includePaths = require __DIR__ . '/include_paths.php'; array_push($includePaths, get_include_path()); @@ -48,7 +48,7 @@ class ComposerAutoloaderInit788374cf70ca1db1fb5ad2e33550a92a } } -function composerRequire788374cf70ca1db1fb5ad2e33550a92a($file) +function composerRequire89af41ceda5657caa52b70dbf0e455f1($file) { require $file; }