diff --git a/application/Espo/Core/Mail/Importer.php b/application/Espo/Core/Mail/Importer.php index b22f99c0f7..48bb24ab20 100644 --- a/application/Espo/Core/Mail/Importer.php +++ b/application/Espo/Core/Mail/Importer.php @@ -64,11 +64,15 @@ class Importer return $this->filtersMatcher; } - public function importMessage($message, $assignedUserId = null, $teamsIdList = [], $userIdList = [], $filterList = [], $fetchOnlyHeader = false, $folderData = null, $parserType = 'Zend') + public function importMessage($message, $assignedUserId = null, $teamsIdList = [], $userIdList = [], $filterList = [], $fetchOnlyHeader = false, $folderData = null, $parserType = 'ZendMail') { try { + $parser = $message->getParser(); $parserClassName = '\\Espo\\Core\\Mail\\Parsers\\' . $parserType; - $parser = new $parserClassName($this->getEntityManager()); + + if (get_class($parser) !== $parserClassName) { + $parser = new $parserClassName($this->getEntityManager()); + } $email = $this->getEntityManager()->getEntity('Email'); @@ -105,14 +109,14 @@ class Importer $email->set('fromString', $parser->getMessageAttribute($message, 'from')); } - if ($parser->checkMessageAttribute($message, 'replyTo')) { - $email->set('replyToString', $parser->getMessageAttribute($message, 'replyTo')); + if ($parser->checkMessageAttribute($message, 'reply-To')) { + $email->set('replyToString', $parser->getMessageAttribute($message, 'reply-To')); } $fromArr = $parser->getAddressListFromMessage($message, 'from'); $toArr = $parser->getAddressListFromMessage($message, 'to'); $ccArr = $parser->getAddressListFromMessage($message, 'cc'); - $replyToArr = $parser->getAddressListFromMessage($message, 'replyTo'); + $replyToArr = $parser->getAddressListFromMessage($message, 'reply-To'); $email->set('from', $fromArr[0]); $email->set('to', implode(';', $toArr)); @@ -129,12 +133,12 @@ class Importer return false; } - if ($parser->checkMessageAttribute($message, 'messageId') && $parser->getMessageAttribute($message, 'messageId')) { + if ($parser->checkMessageAttribute($message, 'message-Id') && $parser->getMessageAttribute($message, 'message-Id')) { $messageId = $parser->getMessageMessageId($message); $email->set('messageId', $messageId); - if ($parser->checkMessageAttribute($message, 'deliveredTo')) { - $email->set('messageIdInternal', $messageId . '-' . $parser->getMessageAttribute($message, 'deliveredTo')); + if ($parser->checkMessageAttribute($message, 'delivered-To')) { + $email->set('messageIdInternal', $messageId . '-' . $parser->getMessageAttribute($message, 'delivered-To')); } if (stripos($messageId, '@espo-system') !== false) { return; @@ -181,12 +185,12 @@ class Importer } else { $email->set('dateSent', date('Y-m-d H:i:s')); } - if ($parser->checkMessageAttribute($message, 'deliveryDate')) { + if ($parser->checkMessageAttribute($message, 'delivery-Date')) { try { - $dt = new \DateTime($parser->getMessageAttribute($message, 'deliveryDate')); + $dt = new \DateTime($parser->getMessageAttribute($message, 'delivery-Date')); if ($dt) { $deliveryDate = $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'); - $email->set('deliveryDate', $deliveryDate); + $email->set('delivery-Date', $deliveryDate); } } catch (\Exception $e) {} } @@ -206,8 +210,8 @@ class Importer $replied = null; - if ($parser->checkMessageAttribute($message, 'inReplyTo') && $parser->getMessageAttribute($message, 'inReplyTo')) { - $arr = explode(' ', $parser->getMessageAttribute($message, 'inReplyTo')); + if ($parser->checkMessageAttribute($message, 'in-Reply-To') && $parser->getMessageAttribute($message, 'in-Reply-To')) { + $arr = explode(' ', $parser->getMessageAttribute($message, 'in-Reply-To')); $inReplyTo = $arr[0]; $replied = $this->getEntityManager()->getRepository('Email')->where(array( 'messageId' => $inReplyTo diff --git a/application/Espo/Core/Mail/Mail/Storage/Imap.php b/application/Espo/Core/Mail/Mail/Storage/Imap.php index 359492a5c9..4eb52f1d76 100644 --- a/application/Espo/Core/Mail/Mail/Storage/Imap.php +++ b/application/Espo/Core/Mail/Mail/Storage/Imap.php @@ -31,8 +31,6 @@ namespace Espo\Core\Mail\Mail\Storage; class Imap extends \Zend\Mail\Storage\Imap { - //protected $messageClass = '\\Espo\\Core\\Mail\\Mail\\Storage\\Message'; - public function getIdsFromUID($uid) { $uid = intval($uid) + 1; @@ -44,5 +42,22 @@ class Imap extends \Zend\Mail\Storage\Imap return $this->protocol->search(array('SINCE "' . $date . '"')); } + public function getHeaderAndFlags($id, $part = null) + { + $data = $this->protocol->fetch(['FLAGS', 'RFC822.HEADER'], $id); + + $header = $data['RFC822.HEADER']; + + $flags = []; + foreach ($data['FLAGS'] as $flag) { + $flags[] = isset(static::$knownFlags[$flag]) ? static::$knownFlags[$flag] : $flag; + } + + return array( + 'flags' => $flags, + 'header' => $header + ); + } + } diff --git a/application/Espo/Core/Mail/MessageWrapper.php b/application/Espo/Core/Mail/MessageWrapper.php new file mode 100644 index 0000000000..98497f587d --- /dev/null +++ b/application/Espo/Core/Mail/MessageWrapper.php @@ -0,0 +1,137 @@ +getHeaderAndFlags($id); + $this->rawHeader = $data['header']; + $this->flagList = $data['flags']; + } + + $this->id = $id; + $this->storage = $storage; + $this->parser = $parser; + } + + public function setFullRawContent($content) + { + $this->fullRawContent = $content; + } + + public function getRawHeader() + { + return $this->rawHeader; + } + + public function getParser() + { + return $this->parser; + } + + public function checkAttribute($attribute) + { + return $this->getParser()->checkMessageAttribute($this, $attribute); + } + + public function getAttribute($attribute) + { + return $this->getParser()->getMessageAttribute($this, $attribute); + } + + public function getRawContent() + { + if (is_null($this->rawContent)) { + $this->rawContent = $this->storage->getRawContent($this->id); + } + + return $this->rawContent; + } + + public function getFullRawContent() + { + if ($this->fullRawContent) { + return $this->fullRawContent; + } + + return $this->getRawHeader() . "\n" . $this->getRawContent(); + } + + public function getZendMessage() + { + if (!$this->zendMessage) { + if ($this->setFullRawContent) { + $this->message = new $this->zendMessageClass([ + 'handler' => $this->storage, + 'raw' => $this->fullRawContent, + 'flags' => $this->flagList + ]); + } else { + $this->message = new $this->zendMessageClass([ + 'handler' => $this->storage, + 'id' => $this->id, + 'headers' => $this->rawHeader, + 'flags' => $this->flagList + ]); + } + } + + return $this->message; + } + + public function getFlags() + { + return $this->flagList; + } + + public function isFetched() + { + return !!$this->rawHeader; + } +} diff --git a/application/Espo/Core/Mail/Parsers/PhpMimeMailParser.php b/application/Espo/Core/Mail/Parsers/PhpMimeMailParser.php new file mode 100644 index 0000000000..b34559e0f1 --- /dev/null +++ b/application/Espo/Core/Mail/Parsers/PhpMimeMailParser.php @@ -0,0 +1,165 @@ +entityManager = $entityManager; + } + + protected function getEntityManager() + { + return $this->entityManager; + } + + protected function getParser($message) + { + $key = spl_object_hash($message); + if (!array_key_exists($key, $this->parserHash)) { + $this->parserHash[$key] = new \PhpMimeMailParser\Parser(); + $raw = $message->getRawHeader(); + if (!$raw) { + $raw = $message->getFullRawContent(); + } + $this->parserHash[$key]->setText($raw); + } + + return $this->parserHash[$key]; + } + + protected function loadContent($message) + { + $this->getParser($message); + + $raw = $message->getFullRawContent(); + $this->getParser($message)->setText($raw); + } + + public function checkMessageAttribute($message, $attribute) + { + return $this->getParser($message)->getHeader($attribute) !== false; + } + + public function getMessageAttribute($message, $attribute) + { + if (!$this->checkMessageAttribute($message, $attribute)) return null; + + return $this->getParser($message)->getHeader($attribute); + } + + public function getMessageMessageId($message) + { + return $this->getMessageAttribute($message, 'Message-ID'); + } + + public function getAddressListFromMessage($message, $type) + { + $addressList = []; + if ($this->checkMessageAttribute($message, $type)) { + $list = $this->getParser($message)->getAddresses($type); + foreach ($list as $address) { + $addressList[] = $address['address']; + } + } + return $addressList; + } + + public function fetchContentParts(\Espo\Entities\Email $email, $message) + { + $this->loadContent($message); + + $bodyPlain = $this->getParser($message)->getMessageBody('text'); + $bodyHtml = $this->getParser($message)->getMessageBody('html'); + if ($bodyHtml) { + $email->set('isHtml', true); + $email->set('body', $bodyHtml); + $email->set('bodyPlain', $bodyPlain); + } else { + $email->set('isHtml', false); + $email->set('body', $bodyPlain); + } + + if (!$email->get('body') && $email->get('bodyPlain')) { + $email->set('body', $email->get('bodyPlain')); + } + + $attachmentObjList = $this->getParser($message)->getAttachments(); + $inlineIds = array(); + + foreach ($attachmentObjList as $attachmentObj) { + $attachment = $this->getEntityManager()->getEntity('Attachment'); + + $content = $attachmentObj->getContent(); + $disposition = $attachmentObj->getContentDisposition(); + + $attachment = $this->getEntityManager()->getEntity('Attachment'); + $attachment->set('name', $attachmentObj->getFileName()); + $attachment->set('type', $attachmentObj->getContentType()); + + if ($disposition == 'inline') { + $attachment->set('role', 'Inline Attachment'); + $contentId = $attachmentObj->getContentID(); + } else { + $attachment->set('role', 'Attachment'); + } + + $attachment->set('contents', $content); + + $this->getEntityManager()->saveEntity($attachment); + + if ($disposition == 'attachment') { + $attachmentsIds = $email->get('attachmentsIds'); + $attachmentsIds[] = $attachment->id; + $email->set('attachmentsIds', $attachmentsIds); + } else if ($disposition == 'inline') { + $inlineIds[$contentId] = $attachment->id; + } + } + + $body = $email->get('body'); + if (!empty($body)) { + foreach ($inlineIds as $cid => $attachmentId) { + if (strpos($body, 'cid:' . $cid) !== false) { + $body = str_replace('cid:' . $cid, '?entryPoint=attachment&id=' . $attachmentId, $body); + } else { + $email->addLinkMultipleId('attachments', $attachmentId); + } + } + $email->set('body', $body); + } + } +} + diff --git a/application/Espo/Core/Mail/Parsers/Zend.php b/application/Espo/Core/Mail/Parsers/ZendMail.php similarity index 97% rename from application/Espo/Core/Mail/Parsers/Zend.php rename to application/Espo/Core/Mail/Parsers/ZendMail.php index 8bdffa36f9..a4d333c5d3 100644 --- a/application/Espo/Core/Mail/Parsers/Zend.php +++ b/application/Espo/Core/Mail/Parsers/ZendMail.php @@ -29,7 +29,7 @@ namespace Espo\Core\Mail\Parsers; -class Zend +class ZendMail { private $entityManager; @@ -38,18 +38,22 @@ class Zend $this->entityManager = $entityManager; } - public function getEntityManager() + protected function getEntityManager() { return $this->entityManager; } public function checkMessageAttribute($message, $attribute) { + $message = $message->getZendMessage(); + return isset($message->$attribute); } public function getMessageAttribute($message, $attribute) { + $message = $message->getZendMessage(); + if (!isset($message->$attribute)) return null; return $message->$attribute; @@ -57,6 +61,8 @@ class Zend public function getMessageMessageId($message) { + $message = $message->getZendMessage(); + if (!isset($message->messageId)) return null; $messageId = $message->messageId; @@ -68,6 +74,8 @@ class Zend public function getAddressListFromMessage($message, $type) { + $message = $message->getZendMessage(); + $addressList = array(); if (isset($message->$type)) { $list = $this->normilizeHeader($message->getHeader($type))->getAddressList(); @@ -80,6 +88,8 @@ class Zend public function fetchContentParts(\Espo\Entities\Email $email, $message) { + $message = $message->getZendMessage(); + $inlineIds = array(); if ($message->isMultipart()) { diff --git a/application/Espo/Services/EmailAccount.php b/application/Espo/Services/EmailAccount.php index 044bfb0971..f8fc87ef81 100644 --- a/application/Espo/Services/EmailAccount.php +++ b/application/Espo/Services/EmailAccount.php @@ -225,6 +225,18 @@ class EmailAccount extends Record $portionLimit = $this->getConfig()->get('personalEmailMaxPortionSize', self::PORTION_LIMIT); + $parserName = 'ZendMail'; + if (extension_loaded('mailparse')) { + $parserName = 'PhpMimeMailParser'; + } + + if ($this->getConfig()->get('emailParser')) { + $parserName = $this->getConfig()->get('emailParser'); + } + + $parserClassName = '\\Espo\\Core\\Mail\\Parsers\\' . $parserName; + $parser = new $parserClassName($this->getEntityManager()); + $monitoredFoldersArr = explode(',', $monitoredFolders); foreach ($monitoredFoldersArr as $folder) { $folder = mb_convert_encoding(trim($folder), 'UTF7-IMAP', 'UTF-8'); @@ -288,15 +300,15 @@ class EmailAccount extends Record $message = null; $email = null; try { - $message = $storage->getMessage($id); - if ($message && $emailAccount->get('keepFetchedEmailsUnread')) { + $message = new \Espo\Core\Mail\MessageWrapper($storage, $id, $parser); + + if ($message->isFetched() && $emailAccount->get('keepFetchedEmailsUnread')) { $flags = $message->getFlags(); } - try { - $email = $importer->importMessage($message, null, $teamIdList, [$userId], $filterCollection, $fetchOnlyHeader, $folderData); - } catch (\Exception $e) { - $GLOBALS['log']->error('EmailAccount '.$emailAccount->id.' (Import Message): [' . $e->getCode() . '] ' .$e->getMessage()); - } + + $importMethodName = 'importWith' . $parserName; + + $email = $this->$importMethodName($importer, $emailAccount, $message, $teamIdList, null, [$userId], $filterCollection, $fetchOnlyHeader, $folderData); if ($emailAccount->get('keepFetchedEmailsUnread')) { if (is_array($flags) && empty($flags[Storage::FLAG_SEEN])) { @@ -305,7 +317,7 @@ class EmailAccount extends Record } } catch (\Exception $e) { - $GLOBALS['log']->error('EmailAccount '.$emailAccount->id.' (Get Message): [' . $e->getCode() . '] ' .$e->getMessage()); + $GLOBALS['log']->error('EmailAccount '.$emailAccount->id.' (Get Message w/ parser '.$parserName.'): [' . $e->getCode() . '] ' .$e->getMessage()); } if (!empty($email)) { @@ -318,10 +330,10 @@ class EmailAccount extends Record if ($k == count($ids) - 1) { $lastUID = $storage->getUniqueId($id); - if ($message && isset($message->date)) { + if ($email && $email->get('dateSent')) { $dt = null; try { - $dt = new \DateTime($message->date); + $dt = new \DateTime($email->get('dateSent')); } catch (\Exception $e) {} if ($dt) { @@ -350,6 +362,28 @@ class EmailAccount extends Record return true; } + protected function importWithPhpMimeMailParser($importer, $emailAccount, $message, $teamIdList, $userId = null, $userIdList = [], $filterCollection, $fetchOnlyHeader, $folderData = null) + { + $email = null; + try { + $email = $importer->importMessage($message, $userId, $teamIdList, $userIdList, $filterCollection, $fetchOnlyHeader, $folderData, 'PhpMimeMailParser'); + } catch (\Exception $e) { + $GLOBALS['log']->error('EmailAccount '.$emailAccount->id.' (Import Message w/ php-mime-mail-parser): [' . $e->getCode() . '] ' .$e->getMessage()); + } + return $email; + } + + protected function importWithZendMail($importer, $emailAccount, $message, $teamIdList, $userId = null, $userIdList = [], $filterCollection, $fetchOnlyHeader, $folderData = null) + { + $email = null; + try { + $email = $importer->importMessage($message, $userId, $teamIdList, $userIdList, $filterCollection, $fetchOnlyHeader, $folderData, 'ZendMail'); + } catch (\Exception $e) { + $GLOBALS['log']->error('EmailAccount '.$emailAccount->id.' (Import Message w/ zend-mail): [' . $e->getCode() . '] ' .$e->getMessage()); + } + return $email; + } + protected function noteAboutEmail($email) { if ($email->get('parentType') && $email->get('parentId')) { diff --git a/application/Espo/Services/InboundEmail.php b/application/Espo/Services/InboundEmail.php index 7aa67cc8fe..d7491f984a 100644 --- a/application/Espo/Services/InboundEmail.php +++ b/application/Espo/Services/InboundEmail.php @@ -226,6 +226,18 @@ class InboundEmail extends \Espo\Services\Record $monitoredFolders = 'INBOX'; } + $parserName = 'ZendMail'; + if (extension_loaded('mailparse')) { + $parserName = 'PhpMimeMailParser'; + } + + if ($this->getConfig()->get('emailParser')) { + $parserName = $this->getConfig()->get('emailParser'); + } + + $parserClassName = '\\Espo\\Core\\Mail\\Parsers\\' . $parserName; + $parser = new $parserClassName($this->getEntityManager()); + $portionLimit = $this->getConfig()->get('inboundEmailMaxPortionSize', self::PORTION_LIMIT); $monitoredFoldersArr = explode(',', $monitoredFolders); @@ -239,7 +251,6 @@ class InboundEmail extends \Espo\Services\Record continue; } - $lastUID = 0; $lastDate = 0; if (!empty($fetchData->lastUID->$folder)) { @@ -288,9 +299,11 @@ class InboundEmail extends \Espo\Services\Record $message = null; $email = null; try { - $message = $storage->getMessage($id); - if ($message && isset($message->from)) { - $fromString = $message->from; + $message = new \Espo\Core\Mail\MessageWrapper($storage, $id, $parser); + + if ($message && $message->checkAttribute('from')) { + $fromString = $message->getAttribute('from'); + if (preg_match('/MAILER-DAEMON|POSTMASTER/i', $fromString)) { $toSkip = true; try { @@ -301,14 +314,12 @@ class InboundEmail extends \Espo\Services\Record } } if (!$toSkip) { - try { - $email = $importer->importMessage($message, $userId, $teamIdList, $userIdList, $filterCollection, $fetchOnlyHeader); - } catch (\Exception $e) { - $GLOBALS['log']->error('InboundEmail '.$emailAccount->id.' (Import Message): [' . $e->getCode() . '] ' .$e->getMessage()); - } + $importMethodName = 'importWith' . $parserName; + + $email = $this->$importMethodName($importer, $emailAccount, $message, $teamIdList, $userId, $userIdList, $filterCollection, $fetchOnlyHeader, null); } } catch (\Exception $e) { - $GLOBALS['log']->error('InboundEmail '.$emailAccount->id.' (Get Message): [' . $e->getCode() . '] ' .$e->getMessage()); + $GLOBALS['log']->error('InboundEmail '.$emailAccount->id.' (Get Message w/ parser '.$parserName.'): [' . $e->getCode() . '] ' .$e->getMessage()); } try { @@ -335,10 +346,12 @@ class InboundEmail extends \Espo\Services\Record } if ($k == count($ids) - 1) { - if ($message && isset($message->date)) { + $lastUID = $storage->getUniqueId($id); + + if ($email && $email->get('dateSent')) { $dt = null; try { - $dt = new \DateTime($message->date); + $dt = new \DateTime($email->get('dateSent')); } catch (\Exception $e) {} if ($dt) { @@ -367,6 +380,28 @@ class InboundEmail extends \Espo\Services\Record return true; } + protected function importWithPhpMimeMailParser($importer, $emailAccount, $message, $teamIdList, $userId = null, $userIdList = [], $filterCollection, $fetchOnlyHeader, $folderData = null) + { + $email = null; + try { + $email = $importer->importMessage($message, $userId, $teamIdList, $userIdList = [], $filterCollection, $fetchOnlyHeader, $folderData, 'PhpMimeMailParser'); + } catch (\Exception $e) { + $GLOBALS['log']->error('InboundEmail '.$emailAccount->id.' (Import Message w/ php-mime-mail-parser): [' . $e->getCode() . '] ' .$e->getMessage()); + } + return $email; + } + + protected function importWithZendMail($importer, $emailAccount, $message, $teamIdList, $userId = null, $userIdList = [], $filterCollection, $fetchOnlyHeader, $folderData = null) + { + $email = null; + try { + $email = $importer->importMessage($message, $userId, $teamIdList, $userIdList, $filterCollection, $fetchOnlyHeader, $folderData, 'ZendMail'); + } catch (\Exception $e) { + $GLOBALS['log']->error('InboundEmail '.$emailAccount->id.' (Import Message w/ zend-mail): [' . $e->getCode() . '] ' .$e->getMessage()); + } + return $email; + } + protected function noteAboutEmail($email) { if ($email->get('parentType') && $email->get('parentId')) { @@ -643,9 +678,9 @@ class InboundEmail extends \Espo\Services\Record } catch (\Exception $e) {} } - protected function processBouncedMessage(\Zend\Mail\Storage\Message $message) + protected function processBouncedMessage($message) { - $content = $message->getContent(); + $content = $message->getRawContent(); $isHard = false; if (preg_match('/permanent[ ]*[error|failure]/', $content)) { diff --git a/composer.json b/composer.json index 63921063a6..a71ea5a3c0 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "zordius/lightncandy": "0.*", "composer/semver": "^1.4", "zendframework/zend-servicemanager": "^3.1", - "tecnickcom/tcpdf": "^6.2" + "tecnickcom/tcpdf": "^6.2", + "php-mime-mail-parser/php-mime-mail-parser": "^2.5" }, "autoload": { "psr-0": { diff --git a/composer.lock b/composer.lock index 19fd5c76b7..e5f74f354a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "46075698dc4d8c1138cae376ebc572c9", - "content-hash": "b941abc20f072ef97ac9ac3f8c53f5c7", + "hash": "bbe970605dccdfcb703299fb5062a97a", + "content-hash": "d8b34e846d35918eeb1cbee0ac16091e", "packages": [ { "name": "composer/semver", @@ -686,6 +686,86 @@ ], "time": "2013-11-23 19:48:39" }, + { + "name": "php-mime-mail-parser/php-mime-mail-parser", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/php-mime-mail-parser/php-mime-mail-parser.git", + "reference": "9e4da9b71fa653782fa622ddb4363709aabcb714" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mime-mail-parser/php-mime-mail-parser/zipball/9e4da9b71fa653782fa622ddb4363709aabcb714", + "reference": "9e4da9b71fa653782fa622ddb4363709aabcb714", + "shasum": "" + }, + "require": { + "ext-mailparse": "*", + "php": "^5.4.0 || ^7.0" + }, + "replace": { + "exorus/php-mime-mail-parser": "*", + "messaged/php-mime-mail-parser": "*" + }, + "require-dev": { + "phpunit/php-token-stream": "^1.3.0", + "phpunit/phpunit": "^4.0 || ^5.0", + "satooshi/php-coveralls": "0.*", + "squizlabs/php_codesniffer": "2.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpMimeMailParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "bucabay", + "email": "gabe@fijiwebdesign.com", + "homepage": "http://www.fijiwebdesign.com", + "role": "Developer" + }, + { + "name": "eXorus", + "email": "exorus.spam@gmail.com", + "homepage": "https://github.com/eXorus/", + "role": "Developer" + }, + { + "name": "M.Valinskis", + "email": "M.Valins@gmail.com", + "homepage": "https://code.google.com/p/php-mime-mail-parser", + "role": "Developer" + }, + { + "name": "eugene.emmett.wood", + "email": "gene_w@cementhorizon.com", + "homepage": "https://code.google.com/p/php-mime-mail-parser", + "role": "Developer" + }, + { + "name": "alknetso", + "email": "alkne@gmail.com", + "homepage": "https://code.google.com/p/php-mime-mail-parser", + "role": "Developer" + } + ], + "description": "Fully Tested Mailparse Extension Wrapper for PHP 5.4+", + "homepage": "https://github.com/php-mime-mail-parser/php-mime-mail-parser", + "keywords": [ + "MimeMailParser", + "mail", + "mailparse", + "mime" + ], + "time": "2016-10-07 16:46:22" + }, { "name": "psr/log", "version": "1.0.0",