user = $user; $this->entityManager = $entityManager; $this->fieldValidationManager = $fieldValidationManager; $this->emailSender = $emailSender; $this->streamService = $streamService; $this->config = $config; $this->log = $log; $this->injectableFactory = $injectableFactory; $this->acl = $acl; $this->accountProvider = $accountProvider; $this->personalAccountService = $personalAccountService; $this->groupAccountService = $groupAccountService; $this->handlerProcessor = $handlerProcessor; $this->personalAccountFactory = $personalAccountFactory; $this->groupAccountFactory = $groupAccountFactory; } /** * Send an email entity. * * @params Email $entity An email entity. * @params ?User $user A user from what to send. * * @throws BadRequest If not valid. * @throws SendingError On error while sending. * @throws NoSmtp No SMTP settings. * @throws Error An error. */ public function send(Email $entity, ?User $user = null): void { if (in_array($entity->getStatus(), $this->notAllowedStatusList)) { throw new Error("Can't send email with status `{$entity->getStatus()}`."); } if (!$this->fieldValidationManager->check($entity, 'to', 'required')) { $entity->setStatus(Email::STATUS_DRAFT); $this->entityManager->saveEntity($entity, [SaveOption::SILENT => true]); throw new BadRequest("Empty To address."); } $systemIsShared = $this->config->get('outboundEmailIsShared'); $systemFromName = $this->config->get('outboundEmailFromName'); $systemFromAddress = $this->config->get('outboundEmailFromAddress'); $emailSender = $this->emailSender->create(); $userAddressList = []; if ($user) { /** @var Collection $emailAddressCollection */ $emailAddressCollection = $this->entityManager ->getRDBRepositoryByClass(User::class) ->getRelation($user, 'emailAddresses') ->find(); foreach ($emailAddressCollection as $ea) { $userAddressList[] = $ea->getLower(); } } $originalFromAddress = $entity->getFromAddress(); if (!$originalFromAddress) { throw new Error("Email sending: Can't send with empty 'from' address."); } $fromAddress = strtolower($originalFromAddress); $isUserAddress = in_array($fromAddress, $userAddressList); $isSystemAddress = $fromAddress === strtolower($systemFromAddress); $smtpParams = null; $personalAccount = null; $groupAccount = null; $params = SenderParams::create(); if ($user && $isUserAddress) { [$smtpParams, $personalAccount] = $this->getPersonalAccount($user, $originalFromAddress); } if ($user && $smtpParams) { // For bc. $smtpParams = $this->applyUserHandler($user, $smtpParams, $fromAddress); $emailSender->withSmtpParams($smtpParams); } if (!$smtpParams) { [$smtpParams, $groupAccount] = $this->getGroupAccount($user, $originalFromAddress); if ($smtpParams) { $emailSender->withSmtpParams($smtpParams); } } if (!$smtpParams && $isSystemAddress) { $params = $params->withFromName($systemFromName); } // Otherwise, allow users to send from the system SMTP with their own from-address. if (!$smtpParams && !$systemIsShared) { if ($isSystemAddress) { throw new NoSmtp("Can not use system SMTP. System SMTP is not shared."); } throw new NoSmtp("No SMTP params for {$fromAddress}."); } if ( !$smtpParams && $user && in_array($fromAddress, $userAddressList) ) { $params = $params->withFromName($user->getName()); } $parent = null; $parentId = $entity->getParentId(); $parentType = $entity->getParentType(); if ($parentType && $parentId) { $parent = $this->entityManager->getEntityById($parentType, $parentId); $params = $this->applyParent($parent, $params); } $this->validateEmailAddresses($entity); $message = new Message(); $repliedMessageId = $this->getRepliedEmailMessageId($entity); if ($repliedMessageId) { $message->getHeaders()->addHeaderLine('In-Reply-To', $repliedMessageId); $message->getHeaders()->addHeaderLine('References', $repliedMessageId); } try { $emailSender ->withParams($params) ->withMessage($message) ->send($entity); } catch (Exception $e) { $entity->setStatus(Email::STATUS_DRAFT); $this->entityManager->saveEntity($entity, [SaveOption::SILENT => true]); $this->log->error("Email sending:" . $e->getMessage() . "; " . $e->getCode()); $errorData = [ 'id' => $entity->getId(), 'message' => $e->getMessage(), ]; throw ErrorSilent::createWithBody('sendingFail', Json::encode($errorData)); } if ($groupAccount) { $groupAccountId = $groupAccount->getId(); if ($groupAccountId) { $entity->addLinkMultipleId('inboundEmails', $groupAccountId); } } if ($personalAccount) { $personalAccountId = $personalAccount->getId(); if ($personalAccountId) { $entity->addLinkMultipleId('emailAccounts', $personalAccountId); } } $this->entityManager->saveEntity($entity, ['isJustSent' => true]); $this->store($message, $groupAccount, $personalAccount); if ($parent) { $this->streamService->noteEmailSent($parent, $entity); } } private function applyParent(?Entity $parent, SenderParams $params): SenderParams { // @todo Refactor. Move to a separate class? Make extensible? if ($parent instanceof CaseObj) { $inboundEmailId = $parent->getInboundEmailId(); if (!$inboundEmailId) { return $params; } /** @var ?InboundEmail $inboundEmail */ $inboundEmail = $this->entityManager ->getRDBRepositoryByClass(InboundEmail::class) ->getById($inboundEmailId); if (!$inboundEmail || !$inboundEmail->getReplyToAddress()) { return $params; } $params = $params->withReplyToAddress($inboundEmail->getReplyToAddress()); } return $params; } private function store(Message $message, ?Account $groupAccount, ?Account $personalAccount): void { if ($groupAccount instanceof GroupAccount && $groupAccount->storeSentEmails()) { $id = $groupAccount->getId() ?? null; if (!$id) { throw new LogicException(); } try { $this->groupAccountService->storeSentMessage($id, $message); } catch (Exception $e) { $this->log->error( "Email sending: Could not store sent email (Group Email Account {$groupAccount->getId()}): " . $e->getMessage() . "." ); } } if ($personalAccount instanceof PersonalAccount && $personalAccount->storeSentEmails()) { $id = $personalAccount->getId() ?? null; if (!$id) { throw new LogicException(); } try { $this->personalAccountService->storeSentMessage($id, $message); } catch (Exception $e) { $this->log->error( "Email sending: Could not store sent email (Email Account {$personalAccount->getId()}): " . $e->getMessage() . "." ); } } } /** * @return array{?SmtpParams, ?Account} */ private function getPersonalAccount(User $user, string $emailAddress): array { $personalAccount = $this->accountProvider->getPersonal($user, $emailAddress); if (!$personalAccount) { return [null, null]; } if (!$personalAccount->isAvailableForSending()) { return [null, null]; } $smtpParams = $personalAccount->getSmtpParams(); if (!$smtpParams) { return [null, null]; } $smtpParams = $smtpParams->withFromName($user->getName()); return [$smtpParams, $personalAccount]; } /** * @return array{?SmtpParams, ?Account} */ private function getGroupAccount(?User $user, string $emailAddress): array { $groupAccount = $user ? $this->accountProvider->getShared($user, $emailAddress) : $this->accountProvider->getGroup($emailAddress); if (!$groupAccount) { return [null, null]; } $smtpParams = $groupAccount->getSmtpParams(); if (!$smtpParams) { return [null, null]; } return [$smtpParams, $groupAccount]; } private function applyUserHandler(User $user, SmtpParams $smtpParams, string $emailAddress): SmtpParams { $raw = $smtpParams->toArray(); $applied = $this->applyUserHandlerInternal($user->getId(), $emailAddress, $raw); if ($applied) { return SmtpParams::fromArray($raw); } return $smtpParams; } /** * @throws Forbidden * @throws Error * @throws NotFound */ public function sendTestEmail(SmtpParams $params, TestSendData $data): void { $emailAddress = $data->getEmailAddress(); $userId = $data->getUserId(); $type = $data->getType(); $id = $data->getId(); if ($params->getPassword() === null) { $params = $params->withPassword( $this->obtainSendTestEmailPassword($type, $id) ); } $fromAddress = $params->getFromAddress(); if ( $userId && $userId !== $this->user->getId() && !$this->user->isAdmin() ) { throw new Forbidden(); } /** @var ?User $user */ $user = $userId ? $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId) : null; if ($userId && !$user) { throw new NotFound("User not found."); } /** @var Email $email */ $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); $email ->setSubject('EspoCRM: Test Email') ->setIsHtml(false) ->addToAddress($emailAddress); $handlerClassName = null; if ($type === 'emailAccount' && $id) { /** @var ?EmailAccount $emailAccount */ $emailAccount = $this->entityManager->getEntityById(EmailAccount::ENTITY_TYPE, $id); if ($emailAccount) { $handlerClassName = $emailAccount->getSmtpHandlerClassName(); } } if ($type === 'inboundEmail' && $id) { /** @var ?InboundEmail $inboundEmail */ $inboundEmail = $this->entityManager->getEntityById(InboundEmail::ENTITY_TYPE, $id); if ($inboundEmail) { $handlerClassName = $inboundEmail->getSmtpHandlerClassName(); } } if ($handlerClassName && $id) { $params = $this->handlerProcessor->handle($handlerClassName, $params, $id); } if ($user && $fromAddress) { $params = $this->applyUserHandler($user, $params, $fromAddress); } $emailSender = $this->emailSender; try { $emailSender ->withSmtpParams($params) ->send($email); } catch (Exception $e) { $this->log->warning("Email sending:" . $e->getMessage() . "; " . $e->getCode()); $errorData = ['message' => $e->getMessage()]; throw ErrorSilent::createWithBody('sendingFail', Json::encode($errorData)); } } /** * @throws Error */ public function validateEmailAddresses(Email $entity): void { $from = $entity->getFromAddress(); if ($from) { if (!filter_var($from, \FILTER_VALIDATE_EMAIL)) { throw new Error('From email address is not valid.'); } } foreach ($entity->getToAddressList() as $address) { if (!filter_var($address, \FILTER_VALIDATE_EMAIL)) { throw new Error('To email address is not valid.'); } } foreach ($entity->getCcAddressList() as $address) { if (!filter_var($address, \FILTER_VALIDATE_EMAIL)) { throw new Error('CC email address is not valid.'); } } foreach ($entity->getBccAddressList() as $address) { if (!filter_var($address, \FILTER_VALIDATE_EMAIL)) { throw new Error('BCC email address is not valid.'); } } } /** * Get a user personal SMTP params. */ public function getUserSmtpParams(string $userId): ?SmtpParams { /** @var ?User $user */ $user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId); if (!$user) { return null; } $address = $user->getEmailAddress(); if (!$address) { return null; } $account = $this->accountProvider->getPersonal($user, $address); if (!$account) { return null; } $smtpParams = $account->getSmtpParams(); if (!$smtpParams) { return null; } // For bc. $smtpParams = $this->applyUserHandler($user, $smtpParams, strtolower($address)); return $smtpParams ->withFromName($user->getName()) ->withFromAddress($address); } private function getRepliedEmailMessageId(Email $email): ?string { $repliedLink = $email->getReplied(); if (!$repliedLink) { return null; } /** @var ?Email $replied */ $replied = $this->entityManager ->getRDBRepositoryByClass(Email::class) ->select(['messageId']) ->where(['id' => $repliedLink->getId()]) ->findOne(); if (!$replied) { return null; } return $replied->getMessageId(); } /** * @internal For bc. * @param array $params */ private function applyUserHandlerInternal(string $userId, string $emailAddress, array &$params): bool { $userData = $this->getUserDataRepository()->getByUserId($userId); if (!$userData) { return false; } $smtpHandlers = $userData->get('smtpHandlers') ?? (object) []; if (!is_object($smtpHandlers)) { return false; } if (!isset($smtpHandlers->$emailAddress)) { return false; } /** @var class-string $handlerClassName */ $handlerClassName = $smtpHandlers->$emailAddress; try { $handler = $this->injectableFactory->create($handlerClassName); } catch (Throwable $e) { $this->log->error( "Email sending: Could not create Smtp Handler for {$emailAddress}. Error: " . $e->getMessage() . "." ); return false; } if (method_exists($handler, 'applyParams')) { $handler->applyParams($userId, $emailAddress, $params); return true; } return false; } /** * @throws Forbidden * @throws NotFound * @throws Error * @throws NoSmtp */ private function obtainSendTestEmailPassword(?string $type, ?string $id): ?string { if ($type === 'emailAccount') { if (!$this->acl->checkScope(EmailAccount::ENTITY_TYPE)) { throw new Forbidden(); } if (!$id) { return null; } $personalAccount = $this->personalAccountFactory->create($id); if ( !$this->user->isAdmin() && $personalAccount->getUser()->getId() !== $id ) { throw new Forbidden(); } $smtpParams = $personalAccount->getSmtpParams(); if (!$smtpParams) { return null; } return $smtpParams->getPassword(); } if (!$this->user->isAdmin()) { throw new Forbidden(); } if ($type === 'inboundEmail') { if (!$id) { return null; } $smtpParams = $this->groupAccountFactory ->create($id) ->getSmtpParams(); if (!$smtpParams) { return null; } return $smtpParams->getPassword(); } return $this->config->get('smtpPassword'); } private function getUserDataRepository(): UserDataRepository { /** @var UserDataRepository */ return $this->entityManager->getRepository(UserData::ENTITY_TYPE); } }