entityManager = $entityManager; $this->config = $config; $this->parserFactory = $parserFactory; $this->linkMultipleSaver = $linkMultipleSaver; $this->duplicateFinder = $duplicateFinder; $this->jobSchedulerFactory = $jobSchedulerFactory; $this->notificator = $notificatorFactory->create(Email::ENTITY_TYPE); $this->filtersMatcher = new FiltersMatcher(); } public function import(Message $message, Data $data): ?Email { $assignedUserId = $data->getAssignedUserId(); $teamIdList = $data->getTeamIdList(); $userIdList = $data->getUserIdList(); $filterList = $data->getFilterList(); $folderData = $data->getFolderData(); $groupEmailFolderId = $data->getGroupEmailFolderId(); $parser = $message instanceof MessageWrapper ? ($message->getParser() ?? $this->parserFactory->create()) : $this->parserFactory->create(); /** @var Email $email */ $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); $email->set('isBeingImported', true); $subject = ''; if ($parser->hasHeader($message, 'subject')) { $subject = $parser->getHeader($message, 'subject'); } if (!empty($subject) && is_string($subject)) { $subject = trim($subject); } if ($subject !== '0' && empty($subject)) { $subject = '(No Subject)'; } if (strlen($subject) > self::SUBJECT_MAX_LENGTH) { $subject = substr($subject, 0, self::SUBJECT_MAX_LENGTH); } $email->set('isHtml', false); $email->set('name', $subject); $email->set('status', Email::STATUS_ARCHIVED); $email->set('attachmentsIds', []); $email->set('teamsIds', $teamIdList); $email->set('groupFolderId', $groupEmailFolderId); if ($assignedUserId) { $email->set('assignedUserId', $assignedUserId); $email->addLinkMultipleId('assignedUsers', $assignedUserId); } foreach ($userIdList as $uId) { $email->addLinkMultipleId('users', $uId); } $fromAddressData = $parser->getAddressData($message, 'from'); if ($fromAddressData) { $fromString = ($fromAddressData->name ? ($fromAddressData->name . ' ') : '') . '<' . $fromAddressData->address . '>'; $email->set('fromString', $fromString); } $replyToData = $parser->getAddressData($message, 'reply-To'); if ($replyToData) { $replyToString = ($replyToData->name ? ($replyToData->name . ' ') : '') . '<' . $replyToData->address . '>'; $email->set('replyToString', $replyToString); } $fromArr = $parser->getAddressList($message, 'from'); $toArr = $parser->getAddressList($message, 'to'); $ccArr = $parser->getAddressList($message, 'cc'); $replyToArr = $parser->getAddressList($message, 'reply-To'); if (count($fromArr)) { $email->set('from', $fromArr[0]); } $email->set('to', implode(';', $toArr)); $email->set('cc', implode(';', $ccArr)); $email->set('replyTo', implode(';', $replyToArr)); $addressNameMap = $parser->getAddressNameMap($message); $email->set('addressNameMap', $addressNameMap); foreach ($folderData as $uId => $folderId) { $email->setLinkMultipleColumn('users', 'folderId', $uId, $folderId); } $matchedFilter = $this->filtersMatcher->findMatch($email, $filterList, true); if ($matchedFilter && $matchedFilter->getAction() === EmailFilter::ACTION_SKIP) { return null; } if ($matchedFilter && $matchedFilter->getAction() === EmailFilter::ACTION_MOVE_TO_GROUP_FOLDER) { $groupEmailFolderId = $matchedFilter->getGroupEmailFolderId(); $email->set('groupFolderId', $groupEmailFolderId); } if ( $parser->hasHeader($message, 'message-Id') && $parser->getHeader($message, 'message-Id') ) { /** @var string $messageId */ $messageId = $parser->getMessageId($message); $email->set('messageId', $messageId); if ($parser->hasHeader($message, 'delivered-To')) { $email->set( 'messageIdInternal', $messageId . '-' . $parser->getHeader($message, 'delivered-To') ); } if (stripos($messageId, '@espo-system') !== false) { return null; } } if ($parser->hasHeader($message, 'date')) { try { /** @var string $dateHeaderValue */ $dateHeaderValue = $parser->getHeader($message, 'date'); $dt = new DateTime($dateHeaderValue); $dateSent = $dt ->setTimezone(new DateTimeZone('UTC')) ->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT); $email->set('dateSent', $dateSent); } catch (Exception $e) {} } $duplicate = $this->findDuplicate($email); if ($duplicate && $duplicate->getStatus() !== Email::STATUS_BEING_IMPORTED) { /** @var Email $duplicate */ $duplicate = $this->entityManager->getEntityById(Email::ENTITY_TYPE, $duplicate->getId()); $this->processDuplicate( $duplicate, $assignedUserId, $userIdList, $folderData, $teamIdList, $groupEmailFolderId ); return $duplicate; } if (!$email->getDateSent()) { $email->set('dateSent', date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)); } if ($parser->hasHeader($message, 'delivery-Date')) { try { /** @var string $deliveryDateHeaderValue */ $deliveryDateHeaderValue = $parser->getHeader($message, 'delivery-Date'); $dt = new DateTime($deliveryDateHeaderValue); $deliveryDate = $dt ->setTimezone(new DateTimeZone('UTC')) ->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT); $email->set('deliveryDate', $deliveryDate); } catch (Exception $e) {} } $inlineAttachmentList = []; if (!$data->fetchOnlyHeader()) { $inlineAttachmentList = $parser->getInlineAttachmentList($message, $email); $matchedFilter = $this->filtersMatcher->findMatch($email, $filterList); if ($matchedFilter && $matchedFilter->getAction() === EmailFilter::ACTION_SKIP) { return null; } if ($matchedFilter && $matchedFilter->getAction() === EmailFilter::ACTION_MOVE_TO_GROUP_FOLDER) { $groupEmailFolderId = $matchedFilter->getGroupEmailFolderId(); $email->set('groupFolderId', $groupEmailFolderId); } } else { $email->set('body', 'Not fetched. The email size exceeds the limit.'); $email->set('isHtml', false); } $replied = null; if ( $parser->hasHeader($message, 'in-Reply-To') && $parser->getHeader($message, 'in-Reply-To') ) { $arr = explode(' ', $parser->getHeader($message, 'in-Reply-To')); $inReplyTo = $arr[0]; if ($inReplyTo) { if ($inReplyTo[0] !== '<') { $inReplyTo = '<' . $inReplyTo . '>'; } /** @var Email|null $replied */ $replied = $this->entityManager ->getRDBRepository(Email::ENTITY_TYPE) ->where(['messageId' => $inReplyTo]) ->findOne(); if ($replied) { $email->set('repliedId', $replied->getId()); /** @var string[] $repliedTeamIdList */ $repliedTeamIdList = $replied->getLinkMultipleIdList('teams'); foreach ($repliedTeamIdList as $repliedTeamId) { $email->addLinkMultipleId('teams', $repliedTeamId); } } } } $parentFound = $this->processReferences($parser, $message, $email); if ( !$parentFound && $replied && $replied->getParentId() && $replied->getParentType() ) { /** @var string $parentId */ $parentId = $replied->getParentId(); /** @var string $parentType */ $parentType = $replied->getParentType(); $parentEntity = $this->entityManager->getEntityById($parentType, $parentId); if ($parentEntity) { $parentFound = true; $email->set('parentType', $parentType); $email->set('parentId', $parentId); } } if (!$parentFound) { $from = $email->getFromAddress(); if ($from) { $parentFound = $this->findParentByAddress($email, $from); } } if (!$parentFound) { if (!empty($replyToArr)) { $parentFound = $this->findParentByAddress($email, $replyToArr[0]); } } if (!$parentFound) { if (!empty($toArr)) { $parentFound = $this->findParentByAddress($email, $toArr[0]); } } if (!$duplicate) { $this->entityManager->getLocker()->lockExclusive(Email::ENTITY_TYPE); $duplicate = $this->findDuplicate($email); if ($duplicate) { $this->entityManager->getLocker()->rollback(); if ($duplicate->getStatus() !== Email::STATUS_BEING_IMPORTED) { /** @var Email $duplicate */ $duplicate = $this->entityManager->getEntityById(Email::ENTITY_TYPE, $duplicate->getId()); $this->processDuplicate( $duplicate, $assignedUserId, $userIdList, $folderData, $teamIdList, $groupEmailFolderId ); return $duplicate; } } } if ($duplicate) { $duplicate->set([ 'from' => $email->get('from'), 'to' => $email->get('to'), 'cc' => $email->get('cc'), 'bcc' => $email->get('bcc'), 'replyTo' => $email->get('replyTo'), 'name' => $email->get('name'), 'dateSent' => $email->get('dateSent'), 'body' => $email->get('body'), 'bodyPlain' => $email->get('bodyPlain'), 'parentType' => $email->get('parentType'), 'parentId' => $email->get('parentId'), 'isHtml' => $email->get('isHtml'), 'messageId' => $email->get('messageId'), 'fromString' => $email->get('fromString'), 'replyToString' => $email->get('replyToString'), ]); /** @var EmailRepository $emailRepository */ $emailRepository = $this->entityManager->getRDBRepository(Email::ENTITY_TYPE); $emailRepository->fillAccount($duplicate); $this->processDuplicate( $duplicate, $assignedUserId, $userIdList, $folderData, $teamIdList, $groupEmailFolderId ); return $duplicate; } if (!$email->getMessageId()) { $email->setDummyMessageId(); } $email->set('status', Email::STATUS_BEING_IMPORTED); $this->entityManager->saveEntity($email, [ 'skipAll' => true, 'keepNew' => true, ]); $this->entityManager->getLocker()->commit(); if ($parentFound) { $this->processEmailWithParent($email); } $email->set('status', Email::STATUS_ARCHIVED); $this->entityManager->saveEntity($email, ['isBeingImported' => true]); foreach ($inlineAttachmentList as $attachment) { $attachment->set([ 'relatedId' => $email->getId(), 'relatedType' => Email::ENTITY_TYPE, 'field' => 'body', ]); $this->entityManager->saveEntity($attachment); } return $email; } private function processEmailWithParent(Email $email): void { $parentLink = $email->getParent(); if (!$parentLink) { return; } $parentType = $parentLink->getEntityType(); $parentId = $parentLink->getId(); $emailKeepParentTeamsEntityList = $this->config->get('emailKeepParentTeamsEntityList') ?? []; if ( !in_array($parentType, $emailKeepParentTeamsEntityList) || !$this->entityManager->hasRepository($parentType) ) { return; } $parent = $this->entityManager->getEntity($parentType, $parentId); if (!$parent) { return; } if (!$parent instanceof CoreEntity) { return; } /** @var string[] $parentTeamIdList */ $parentTeamIdList = $parent->getLinkMultipleIdList('teams'); foreach ($parentTeamIdList as $parentTeamId) { $email->addLinkMultipleId('teams', $parentTeamId); } } private function processReferences(Parser $parser, Message $message, Email $email): bool { if ( !$parser->hasHeader($message, 'references') || !$parser->getHeader($message, 'references') ) { return false; } $references = $parser->getHeader($message, 'references'); $delimiter = strpos($references, '>,') ? ',' : ' '; foreach (explode($delimiter, $references) as $reference) { $reference = str_replace(['/', '@'], ' ', trim(trim($reference), '<>')); $parentFound = $this->processReferencesItem($email, $reference); if ($parentFound) { return true; } } return false; } private function processReferencesItem(Email $email, string $reference): bool { $parentType = null; $parentId = null; $number = null; $emailSent = PHP_INT_MAX; $n = sscanf($reference, '%s %s %d %d espo', $parentType, $parentId, $emailSent, $number); if ($n !== 4) { $n = sscanf($reference, '%s %s %d %d espo-system', $parentType, $parentId, $emailSent, $number); } if ($n !== 4 || $emailSent >= time()) { return false; } if (!$parentType || !$parentId) { return false; } $email->set('parentType', $parentType); $email->set('parentId', $parentId); if ($parentType === Lead::ENTITY_TYPE) { /** @var ?Lead $parent */ $parent = $this->entityManager->getEntityById(Lead::ENTITY_TYPE, $parentId); if (!$parent) { $email->set('parentType', null); $email->set('parentId', null); return false; } $this->processReferenceLead($email, $parent); } return true; } private function processReferenceLead(Email $email, Lead $lead): void { if ($lead->getStatus() !== Lead::STATUS_CONVERTED) { return; } $createdAccountId = $lead->get('createdAccountId'); if ($createdAccountId) { $account = $this->entityManager->getEntityById(Account::ENTITY_TYPE, $createdAccountId); if (!$account) { return; } $email->set('parentType', Account::ENTITY_TYPE); $email->set('parentId', $account->getId()); return; } $createdContactId = $lead->get('createdContactId'); if ( $this->config->get('b2cMode') && $createdContactId ) { $contact = $this->entityManager->getEntityById(Contact::ENTITY_TYPE, $createdContactId); if (!$contact) { return; } $email->set('parentType', Contact::ENTITY_TYPE); $email->set('parentId', $contact->getId()); } } private function findParentByAddress(Email $email, string $emailAddress): bool { $contact = $this->entityManager ->getRDBRepository(Contact::ENTITY_TYPE) ->where([ 'emailAddress' => $emailAddress ]) ->findOne(); if ($contact) { if (!$this->config->get('b2cMode') && $contact->get('accountId')) { $email->set('parentType', Account::ENTITY_TYPE); $email->set('parentId', $contact->get('accountId')); return true; } $email->set('parentType', Contact::ENTITY_TYPE); $email->set('parentId', $contact->getId()); return true; } $account = $this->entityManager ->getRDBRepository(Account::ENTITY_TYPE) ->where([ 'emailAddress' => $emailAddress ]) ->findOne(); if ($account) { $email->set('parentType', Account::ENTITY_TYPE); $email->set('parentId', $account->getId()); return true; } $lead = $this->entityManager ->getRDBRepository(Lead::ENTITY_TYPE) ->where([ 'emailAddress' => $emailAddress ]) ->findOne(); if ($lead) { $email->set('parentType', Lead::ENTITY_TYPE); $email->set('parentId', $lead->getId()); return true; } return false; } private function findDuplicate(Email $email): ?Email { return $this->duplicateFinder->find($email); } /** * @param string[] $userIdList * @param array $folderData * @param string[] $teamIdList */ private function processDuplicate( Email $duplicate, ?string $assignedUserId, array $userIdList, array $folderData, array $teamIdList, ?string $groupEmailFolderId ): void { /** @var EmailRepository $emailRepository */ $emailRepository = $this->entityManager->getRDBRepository(Email::ENTITY_TYPE); if ($duplicate->getStatus() == Email::STATUS_ARCHIVED) { $emailRepository->loadFromField($duplicate); $emailRepository->loadToField($duplicate); } $duplicate->loadLinkMultipleField('users'); /** @var string[] $fetchedUserIdList */ $fetchedUserIdList = $duplicate->getLinkMultipleIdList('users'); $duplicate->setLinkMultipleIdList('users', []); $processNoteAcl = false; if ($assignedUserId) { if (!in_array($assignedUserId, $fetchedUserIdList)) { $processNoteAcl = true; $duplicate->addLinkMultipleId('users', $assignedUserId); } $duplicate->addLinkMultipleId('assignedUsers', $assignedUserId); } foreach ($userIdList as $uId) { if (!in_array($uId, $fetchedUserIdList)) { $processNoteAcl = true; $duplicate->addLinkMultipleId('users', $uId); } } foreach ($folderData as $uId => $folderId) { if (!in_array($uId, $fetchedUserIdList)) { $duplicate->setLinkMultipleColumn('users', 'folderId', $uId, $folderId); continue; } $this->entityManager ->getRDBRepository(Email::ENTITY_TYPE) ->getRelation($duplicate, 'users') ->updateColumnsById($uId, ['folderId' => $folderId]); } $duplicate->set('isBeingImported', true); $emailRepository->applyUsersFilters($duplicate); $saverParams = SaverParams::create()->withRawOptions([ 'skipLinkMultipleRemove' => true, 'skipLinkMultipleUpdate' => true, ]); $this->linkMultipleSaver->process($duplicate, 'users', $saverParams); $this->linkMultipleSaver->process($duplicate, 'assignedUsers', $saverParams); if ($this->emailNotificationsEnabled()) { $this->notificator->process( $duplicate, AssignmentNotificatorParams::create()->withRawOptions(['isBeingImported' => true]) ); } /** @var string[] $fetchedTeamIdList */ $fetchedTeamIdList = $duplicate->getLinkMultipleIdList('teams'); foreach ($teamIdList as $teamId) { if (!in_array($teamId, $fetchedTeamIdList)) { $processNoteAcl = true; $this->entityManager ->getRDBRepository(Email::ENTITY_TYPE) ->getRelation($duplicate, 'teams') ->relateById($teamId); } } if ($groupEmailFolderId && !$duplicate->getGroupFolder()) { $this->entityManager ->getRDBRepository(Email::ENTITY_TYPE) ->getRelation($duplicate, 'groupFolder') ->relateById($groupEmailFolderId); } if ($duplicate->getParentType() && $processNoteAcl) { // Need to update acl fields (users and teams) // of notes related to the duplicate email. // To grant access to the user who received the email. $dt = new DateTime(); $dt->modify('+' . self::PROCESS_ACL_DELAY_PERIOD); $this->jobSchedulerFactory ->create() ->setClassName(ProcessNoteAcl::class) ->setData( JobData ::create() ->withTargetId($duplicate->getId()) ->withTargetType(Email::ENTITY_TYPE) ) ->setQueue(QueueName::Q1) ->setTime($dt) ->schedule(); } } private function emailNotificationsEnabled(): bool { return in_array( Email::ENTITY_TYPE, $this->config->get('assignmentNotificationsEntityList') ?? [] ); } }