PageRenderTime 46ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/application/Espo/Core/Mail/Importer.php

https://gitlab.com/johanlindberg/irvato-crm
PHP | 597 lines | 493 code | 76 blank | 28 comment | 152 complexity | 11a3d629270270c41232c422cb177515 MD5 | raw file
  1. <?php
  2. /************************************************************************
  3. * This file is part of EspoCRM.
  4. *
  5. * EspoCRM - Open Source CRM application.
  6. * Copyright (C) 2014-2015 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
  7. * Website: http://www.espocrm.com
  8. *
  9. * EspoCRM is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * EspoCRM is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with EspoCRM. If not, see http://www.gnu.org/licenses/.
  21. *
  22. * The interactive user interfaces in modified source and object code versions
  23. * of this program must display Appropriate Legal Notices, as required under
  24. * Section 5 of the GNU General Public License version 3.
  25. *
  26. * In accordance with Section 7(b) of the GNU General Public License version 3,
  27. * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
  28. ************************************************************************/
  29. namespace Espo\Core\Mail;
  30. use \Zend\Mime\Mime as Mime;
  31. use \Espo\ORM\Entity;
  32. use \Espo\ORM\Email;
  33. class Importer
  34. {
  35. private $entityManager;
  36. private $fileManager;
  37. private $config;
  38. private $filtersMatcher;
  39. public function __construct($entityManager, $fileManager, $config)
  40. {
  41. $this->entityManager = $entityManager;
  42. $this->fileManager = $fileManager;
  43. $this->config = $config;
  44. $this->filtersMatcher = new FiltersMatcher();
  45. }
  46. protected function getEntityManager()
  47. {
  48. return $this->entityManager;
  49. }
  50. protected function getConfig()
  51. {
  52. return $this->config;
  53. }
  54. protected function getFileManager()
  55. {
  56. return $this->fileManager;
  57. }
  58. protected function getFiltersMatcher()
  59. {
  60. return $this->filtersMatcher;
  61. }
  62. public function importMessage($message, $assignedUserId = null, $teamsIdList = [], $userIdList = [], $filterList = [], $fetchOnlyHeader = false, $folderData = null)
  63. {
  64. try {
  65. $email = $this->getEntityManager()->getEntity('Email');
  66. $email->set('isBeingImported', true);
  67. $subject = $message->subject;
  68. if ($subject !== '0' && empty($subject)) {
  69. $subject = '(No Subject)';
  70. }
  71. $email->set('isHtml', false);
  72. $email->set('name', $subject);
  73. $email->set('status', 'Archived');
  74. $email->set('attachmentsIds', []);
  75. if ($assignedUserId) {
  76. $email->set('assignedUserId', $assignedUserId);
  77. $email->addLinkMultipleId('assignedUsers', $assignedUserId);
  78. }
  79. $email->set('teamsIds', $teamsIdList);
  80. if (!empty($userIdList)) {
  81. foreach ($userIdList as $uId) {
  82. $email->addLinkMultipleId('users', $uId);
  83. }
  84. }
  85. $fromArr = $this->getAddressListFromMessage($message, 'from');
  86. if (isset($message->from)) {
  87. $email->set('fromString', $message->from);
  88. }
  89. if (isset($message->replyTo)) {
  90. $email->set('replyToString', $message->replyTo);
  91. }
  92. $toArr = $this->getAddressListFromMessage($message, 'to');
  93. $ccArr = $this->getAddressListFromMessage($message, 'cc');
  94. $replyToArr = $this->getAddressListFromMessage($message, 'replyTo');
  95. $email->set('from', $fromArr[0]);
  96. $email->set('to', implode(';', $toArr));
  97. $email->set('cc', implode(';', $ccArr));
  98. $email->set('replyTo', implode(';', $replyToArr));
  99. if ($folderData) {
  100. foreach ($folderData as $uId => $folderId) {
  101. $email->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
  102. }
  103. }
  104. if ($this->getFiltersMatcher()->match($email, $filterList, true)) {
  105. return false;
  106. }
  107. if (isset($message->messageId) && !empty($message->messageId)) {
  108. $email->set('messageId', $message->messageId);
  109. if (isset($message->deliveredTo)) {
  110. $email->set('messageIdInternal', $message->messageId . '-' . $message->deliveredTo);
  111. }
  112. if (stripos($message->messageId, '@espo-system') !== false) {
  113. return;
  114. }
  115. }
  116. if ($duplicate = $this->findDuplicate($email)) {
  117. if ($assignedUserId) {
  118. $duplicate->addLinkMultipleId('users', $assignedUserId);
  119. $duplicate->addLinkMultipleId('assignedUsers', $assignedUserId);
  120. }
  121. if (!empty($userIdList)) {
  122. foreach ($userIdList as $uId) {
  123. $duplicate->addLinkMultipleId('users', $uId);
  124. }
  125. }
  126. if ($folderData) {
  127. foreach ($folderData as $uId => $folderId) {
  128. $email->setLinkMultipleColumn('users', 'folderId', $uId, $folderId);
  129. }
  130. }
  131. $duplicate->set('isBeingImported', true);
  132. $this->getEntityManager()->saveEntity($duplicate);
  133. if (!empty($teamsIdList)) {
  134. foreach ($teamsIdList as $teamId) {
  135. $this->getEntityManager()->getRepository('Email')->relate($duplicate, 'teams', $teamId);
  136. }
  137. }
  138. return $duplicate;
  139. }
  140. if (isset($message->date)) {
  141. $dt = new \DateTime($message->date);
  142. if ($dt) {
  143. $dateSent = $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
  144. $email->set('dateSent', $dateSent);
  145. }
  146. } else {
  147. $email->set('dateSent', date('Y-m-d H:i:s'));
  148. }
  149. if (isset($message->deliveryDate)) {
  150. $dt = new \DateTime($message->deliveryDate);
  151. if ($dt) {
  152. $deliveryDate = $dt->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
  153. $email->set('deliveryDate', $deliveryDate);
  154. }
  155. }
  156. $inlineIds = array();
  157. if (!$fetchOnlyHeader) {
  158. if ($message->isMultipart()) {
  159. foreach (new \RecursiveIteratorIterator($message) as $part) {
  160. $this->importPartDataToEmail($email, $part, $inlineIds);
  161. }
  162. } else {
  163. $this->importPartDataToEmail($email, $message, $inlineIds, 'text/plain');
  164. }
  165. if (!$email->get('body') && $email->get('bodyPlain')) {
  166. $email->set('body', $email->get('bodyPlain'));
  167. }
  168. $body = $email->get('body');
  169. if (!empty($body)) {
  170. foreach ($inlineIds as $cid => $attachmentId) {
  171. if (strpos($body, 'cid:' . $cid) !== false) {
  172. $body = str_replace('cid:' . $cid, '?entryPoint=attachment&amp;id=' . $attachmentId, $body);
  173. } else {
  174. $email->addLinkMultipleId('attachments', $attachmentId);
  175. }
  176. }
  177. $email->set('body', $body);
  178. }
  179. if ($this->getFiltersMatcher()->matchBody($email, $filterList)) {
  180. return false;
  181. }
  182. } else {
  183. $email->set('body', '(Not fetched)');
  184. $email->set('isHtml', false);
  185. }
  186. $parentFound = false;
  187. $replied = null;
  188. if (isset($message->inReplyTo) && !empty($message->inReplyTo)) {
  189. $arr = explode(' ', $message->inReplyTo);
  190. $inReplyTo = $arr[0];
  191. $replied = $this->getEntityManager()->getRepository('Email')->where(array(
  192. 'messageId' => $inReplyTo
  193. ))->findOne();
  194. if ($replied) {
  195. $email->set('repliedId', $replied->id);
  196. }
  197. }
  198. if (isset($message->references) && !empty($message->references)) {
  199. $arr = explode(' ', $message->references);
  200. $reference = $arr[0];
  201. $reference = str_replace(array('/', '@'), " ", trim($reference, '<>'));
  202. $parentType = $parentId = null;
  203. $emailSent = PHP_INT_MAX;
  204. $n = sscanf($reference, '%s %s %d %d espo', $parentType, $parentId, $emailSent, $number);
  205. if ($n == 4 && $emailSent < time()) {
  206. if (!empty($parentType) && !empty($parentId)) {
  207. if ($parentType == 'Lead') {
  208. $parent = $this->getEntityManager()->getEntity('Lead', $parentId);
  209. if ($parent && $parent->get('status') == 'Converted') {
  210. if ($parent->get('createdAccountId')) {
  211. $account = $this->getEntityManager()->getEntity('Account', $parent->get('createdAccountId'));
  212. if ($account) {
  213. $parentType = 'Account';
  214. $parentId = $account->id;
  215. }
  216. } else {
  217. if ($this->getConfig()->get('b2cMode')) {
  218. if ($parent->get('createdContactId')) {
  219. $contact = $this->getEntityManager()->getEntity('Contact', $parent->get('createdContactId'));
  220. if ($contact) {
  221. $parentType = 'Contact';
  222. $parentId = $contact->id;
  223. }
  224. }
  225. }
  226. }
  227. }
  228. }
  229. $email->set('parentType', $parentType);
  230. $email->set('parentId', $parentId);
  231. $parentFound = true;
  232. }
  233. }
  234. }
  235. if (!$parentFound) {
  236. if ($replied && $replied->get('parentId') && $replied->get('parentType')) {
  237. $parentFound = $this->getEntityManager()->getEntity($replied->get('parentType'), $replied->get('parentId'));
  238. if ($parentFound) {
  239. $email->set('parentType', $replied->get('parentType'));
  240. $email->set('parentId', $replied->get('parentId'));
  241. }
  242. }
  243. }
  244. if (!$parentFound) {
  245. $from = $email->get('from');
  246. if ($from) {
  247. $parentFound = $this->findParent($email, $from);
  248. }
  249. }
  250. if (!$parentFound) {
  251. if (!empty($replyToArr)) {
  252. $parentFound = $this->findParent($email, $replyToArr[0]);
  253. }
  254. }
  255. if (!$parentFound) {
  256. if (!empty($toArr)) {
  257. $parentFound = $this->findParent($email, $toArr[0]);
  258. }
  259. }
  260. $this->getEntityManager()->saveEntity($email);
  261. return $email;
  262. } catch (\Exception $e) {}
  263. }
  264. protected function findParent(Entity $email, $emailAddress)
  265. {
  266. $contact = $this->getEntityManager()->getRepository('Contact')->where(array(
  267. 'emailAddress' => $emailAddress
  268. ))->findOne();
  269. if ($contact) {
  270. if (!$this->getConfig()->get('b2cMode')) {
  271. if ($contact->get('accountId')) {
  272. $email->set('parentType', 'Account');
  273. $email->set('parentId', $contact->get('accountId'));
  274. return true;
  275. }
  276. } else {
  277. $email->set('parentType', 'Contact');
  278. $email->set('parentId', $contact->id);
  279. return true;
  280. }
  281. } else {
  282. $account = $this->getEntityManager()->getRepository('Account')->where(array(
  283. 'emailAddress' => $emailAddress
  284. ))->findOne();
  285. if ($account) {
  286. $email->set('parentType', 'Account');
  287. $email->set('parentId', $account->id);
  288. return true;
  289. } else {
  290. $lead = $this->getEntityManager()->getRepository('Lead')->where(array(
  291. 'emailAddress' => $emailAddress
  292. ))->findOne();
  293. if ($lead) {
  294. $email->set('parentType', 'Lead');
  295. $email->set('parentId', $lead->id);
  296. return true;
  297. }
  298. }
  299. }
  300. }
  301. protected function findDuplicate(Entity $email)
  302. {
  303. if ($email->get('messageId')) {
  304. $duplicate = $this->getEntityManager()->getRepository('Email')->where(array(
  305. 'messageId' => $email->get('messageId')
  306. ))->findOne();
  307. if ($duplicate) {
  308. return $duplicate;
  309. }
  310. }
  311. }
  312. protected function normilizeHeader($header)
  313. {
  314. if (is_a($header, 'ArrayIterator')) {
  315. return $header->current();
  316. } else {
  317. return $header;
  318. }
  319. }
  320. protected function getAddressListFromMessage($message, $type)
  321. {
  322. $addressList = array();
  323. if (isset($message->$type)) {
  324. $list = $this->normilizeHeader($message->getHeader($type))->getAddressList();
  325. foreach ($list as $address) {
  326. $addressList[] = $address->getEmail();
  327. }
  328. }
  329. return $addressList;
  330. }
  331. protected function importPartDataToEmail(\Espo\Entities\Email $email, $part, &$inlineIds = array(), $defaultContentType = null)
  332. {
  333. try {
  334. $type = null;
  335. if ($part->getHeaders() && isset($part->contentType)) {
  336. $type = strtok($part->contentType, ';');
  337. }
  338. $contentDisposition = false;
  339. if (isset($part->ContentDisposition)) {
  340. if (strpos(strtolower($part->ContentDisposition), 'attachment') === 0) {
  341. $contentDisposition = 'attachment';
  342. } else if (strpos(strtolower($part->ContentDisposition), 'inline') === 0) {
  343. $contentDisposition = 'inline';
  344. }
  345. } else if (isset($part->contentID)) {
  346. $contentDisposition = 'inline';
  347. }
  348. if (empty($type)) {
  349. if (!empty($defaultContentType)) {
  350. $type = $defaultContentType;
  351. } else {
  352. return;
  353. }
  354. }
  355. $encoding = null;
  356. $isAttachment = true;
  357. if ($type == 'text/plain' || $type == 'text/html') {
  358. if ($contentDisposition !== 'attachment') {
  359. $isAttachment = false;
  360. $content = $this->getContentFromPart($part);
  361. if ($type == 'text/plain') {
  362. $bodyPlain = '';
  363. if ($email->get('bodyPlain')) {
  364. $bodyPlain .= $email->get('bodyPlain') . "\n";
  365. }
  366. $bodyPlain .= $content;
  367. $email->set('bodyPlain', $bodyPlain);
  368. } else if ($type == 'text/html') {
  369. $body = '';
  370. if ($email->get('body')) {
  371. $body .= $email->get('body') . "<br>";
  372. }
  373. $body .= $content;
  374. $email->set('isHtml', true);
  375. $email->set('body', $body);
  376. }
  377. }
  378. }
  379. if ($isAttachment) {
  380. $content = $part->getContent();
  381. $disposition = null;
  382. $fileName = null;
  383. $contentId = null;
  384. if ($contentDisposition) {
  385. if ($contentDisposition === 'attachment') {
  386. $fileName = $this->fetchFileNameFromContentDisposition($part->ContentDisposition);
  387. if ($fileName) {
  388. $disposition = 'attachment';
  389. }
  390. } else if ($contentDisposition === 'inline') {
  391. if (isset($part->contentID)) {
  392. $contentId = trim($part->contentID, '<>');
  393. $fileName = $contentId;
  394. $disposition = 'inline';
  395. } else {
  396. // for iOS attachments
  397. if (empty($fileName)) {
  398. $fileName = $this->fetchFileNameFromContentDisposition($part->ContentDisposition);
  399. if ($fileName) {
  400. $disposition = 'attachment';
  401. }
  402. }
  403. }
  404. }
  405. }
  406. if (isset($part->contentTransferEncoding)) {
  407. $encoding = strtolower($this->normilizeHeader($part->getHeader('Content-Transfer-Encoding'))->getTransferEncoding());
  408. }
  409. $attachment = $this->getEntityManager()->getEntity('Attachment');
  410. $attachment->set('name', $fileName);
  411. $attachment->set('type', $type);
  412. if ($disposition == 'inline') {
  413. $attachment->set('role', 'Inline Attachment');
  414. } else {
  415. $attachment->set('role', 'Attachment');
  416. }
  417. if ($encoding == 'base64') {
  418. $content = base64_decode($content);
  419. }
  420. $attachment->set('size', strlen($content));
  421. $this->getEntityManager()->saveEntity($attachment);
  422. $path = $this->getEntityManager()->getRepository('Attachment')->getFilePath($attachment);
  423. $this->getFileManager()->putContents($path, $content);
  424. if ($disposition == 'attachment') {
  425. $attachmentsIds = $email->get('attachmentsIds');
  426. $attachmentsIds[] = $attachment->id;
  427. $email->set('attachmentsIds', $attachmentsIds);
  428. } else if ($disposition == 'inline') {
  429. $inlineIds[$contentId] = $attachment->id;
  430. }
  431. }
  432. } catch (\Exception $e) {}
  433. }
  434. protected function decodeAttachmentFileName($fileName)
  435. {
  436. if ($fileName && stripos($fileName, "''") !== false) {
  437. list($encoding, $fileName) = explode("''", $fileName);
  438. $fileName = rawurldecode($fileName);
  439. if (strtoupper($encoding) !== 'UTF-8') {
  440. if ($encoding) {
  441. $fileName = mb_convert_encoding($fileName, 'UTF-8', $encoding);
  442. }
  443. }
  444. }
  445. return $fileName;
  446. }
  447. protected function fetchFileNameFromContentDisposition($contentDisposition)
  448. {
  449. $contentDisposition = preg_replace('/\\\\"/', "{{_!Q!U!O!T!E!_}}", $contentDisposition);
  450. $fileName = false;
  451. $m = array();
  452. if (preg_match('/filename="([^"]+)";?/i', $contentDisposition, $m)) {
  453. $fileName = $m[1];
  454. } else if (preg_match('/filename=([^";]+);?/i', $contentDisposition, $m)) {
  455. $fileName = $m[1];
  456. } else if (preg_match('/filename\*="([^"]+)";?/i', $contentDisposition, $m)) {
  457. $fileName = $m[1];
  458. $fileName = $this->decodeAttachmentFileName($fileName);
  459. } else if (preg_match('/filename\*=([^";]+);?/i', $contentDisposition, $m)) {
  460. $fileName = $m[1];
  461. $fileName = $this->decodeAttachmentFileName($fileName);
  462. } else {
  463. $fileName = '';
  464. foreach (['0', '1'] as $i) {
  465. if (preg_match('/filename\*'.$i.'[\*]?="([^"]+)";?/i', $contentDisposition, $m)) {
  466. $part = $m[1];
  467. $fileName .= $part;
  468. } else if (preg_match('/filename\*'.$i.'[\*]?=([^";]+);?/i', $contentDisposition, $m)) {
  469. $part = $m[1];
  470. $fileName .= $part;
  471. }
  472. }
  473. if ($fileName === '') {
  474. $fileName = null;
  475. } else {
  476. $fileName = $this->decodeAttachmentFileName($fileName);
  477. }
  478. }
  479. if ($fileName) {
  480. $fileName = str_replace('{{_!Q!U!O!T!E!_}}', '"', $fileName);
  481. }
  482. return $fileName;
  483. }
  484. protected function getContentFromPart($part)
  485. {
  486. if ($part instanceof \Zend\Mime\Part) {
  487. $content = $part->getRawContent();
  488. if (strtolower($part->charset) != 'utf-8') {
  489. $content = mb_convert_encoding($content, 'UTF-8', $part->charset);
  490. }
  491. } else {
  492. $content = $part->getContent();
  493. $encoding = null;
  494. if (isset($part->contentTransferEncoding)) {
  495. $cteHeader = $this->normilizeHeader($part->getHeader('Content-Transfer-Encoding'));
  496. $encoding = strtolower($cteHeader->getTransferEncoding());
  497. }
  498. if ($encoding == 'base64') {
  499. $content = base64_decode($content);
  500. }
  501. $charset = 'UTF-8';
  502. if (isset($part->contentType)) {
  503. $ctHeader = $this->normilizeHeader($part->getHeader('Content-Type'));
  504. $charsetParamValue = $ctHeader->getParameter('charset');
  505. if (!empty($charsetParamValue)) {
  506. $charset = strtoupper($charsetParamValue);
  507. }
  508. }
  509. if (isset($part->contentTransferEncoding)) {
  510. $cteHeader = $this->normilizeHeader($part->getHeader('Content-Transfer-Encoding'));
  511. if ($cteHeader->getTransferEncoding() == 'quoted-printable') {
  512. $content = quoted_printable_decode($content);
  513. }
  514. }
  515. if ($charset !== 'UTF-8') {
  516. $content = mb_convert_encoding($content, 'UTF-8', $charset);
  517. }
  518. }
  519. return $content;
  520. }
  521. }