vendor/uvdesk/mailbox-component/Services/MailboxService.php line 38

Open in your IDE?
  1. <?php
  2. namespace Webkul\UVDesk\MailboxBundle\Services;
  3. use Symfony\Component\Yaml\Yaml;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use PhpMimeMailParser\Parser as EmailParser;
  6. use Symfony\Component\HttpFoundation\RequestStack;
  7. use Symfony\Component\DependencyInjection\ContainerInterface;
  8. use Webkul\UVDesk\MailboxBundle\Utils\IMAP;
  9. use Webkul\UVDesk\MailboxBundle\Utils\SMTP;
  10. use Webkul\UVDesk\CoreFrameworkBundle\Entity\User;
  11. use Webkul\UVDesk\CoreFrameworkBundle\Entity\Ticket;
  12. use Webkul\UVDesk\CoreFrameworkBundle\Entity\Thread;
  13. use Webkul\UVDesk\CoreFrameworkBundle\Entity\Website;
  14. use Webkul\UVDesk\MailboxBundle\Utils\Mailbox\Mailbox;
  15. use Webkul\UVDesk\CoreFrameworkBundle\Utils\HTMLFilter;
  16. use Webkul\UVDesk\CoreFrameworkBundle\Entity\SupportRole;
  17. use Webkul\UVDesk\MailboxBundle\Utils\MailboxConfiguration;
  18. use Webkul\UVDesk\CoreFrameworkBundle\Workflow\Events as CoreWorkflowEvents;
  19. use Webkul\UVDesk\CoreFrameworkBundle\SwiftMailer\SwiftMailer as SwiftMailerService;
  20. class MailboxService
  21. {
  22. const PATH_TO_CONFIG = '/config/packages/uvdesk_mailbox.yaml';
  23. private $parser;
  24. private $container;
  25. private $requestStack;
  26. private $entityManager;
  27. private $mailboxCollection = [];
  28. public function __construct(ContainerInterface $container, RequestStack $requestStack, EntityManagerInterface $entityManager, SwiftMailerService $swiftMailer)
  29. {
  30. $this->container = $container;
  31. $this->requestStack = $requestStack;
  32. $this->entityManager = $entityManager;
  33. $this->swiftMailer = $swiftMailer;
  34. }
  35. public function getPathToConfigurationFile()
  36. {
  37. return $this->container->get('kernel')->getProjectDir() . self::PATH_TO_CONFIG;
  38. }
  39. public function createConfiguration($params)
  40. {
  41. $configuration = new MailboxConfigurations\MailboxConfiguration($params);
  42. return $configuration ?? null;
  43. }
  44. public function parseMailboxConfigurations(bool $ignoreInvalidAttributes = false)
  45. {
  46. $path = $this->getPathToConfigurationFile();
  47. if (! file_exists($path)) {
  48. throw new \Exception("File '$path' not found.");
  49. }
  50. // Read configurations from package config.
  51. $mailboxConfiguration = new MailboxConfiguration();
  52. foreach (Yaml::parse(file_get_contents($path))['uvdesk_mailbox']['mailboxes'] ?? [] as $id => $params) {
  53. // Swiftmailer Configuration
  54. $swiftMailerConfigurations = $this->swiftMailer->parseSwiftMailerConfigurations() ?? null;
  55. if (isset($params['smtp_swift_mailer_server'])) {
  56. foreach ($swiftMailerConfigurations as $configuration) {
  57. if ($configuration->getId() == $params['smtp_swift_mailer_server']['mailer_id']) {
  58. $swiftMailerConfiguration = $configuration;
  59. break;
  60. }
  61. }
  62. }
  63. // IMAP Configuration
  64. $imapConfiguration = null;
  65. if (! empty($params['imap_server'])) {
  66. $imapConfiguration = IMAP\Configuration::guessTransportDefinition($params['imap_server']);
  67. if ($imapConfiguration instanceof IMAP\Transport\AppTransportConfigurationInterface) {
  68. $imapConfiguration
  69. ->setClient($params['imap_server']['client'])
  70. ->setUsername($params['imap_server']['username'])
  71. ;
  72. } else if ($imapConfiguration instanceof IMAP\Transport\SimpleTransportConfigurationInterface) {
  73. $imapConfiguration
  74. ->setUsername($params['imap_server']['username'])
  75. ;
  76. } else {
  77. $imapConfiguration
  78. ->setUsername($params['imap_server']['username'])
  79. ->setPassword($params['imap_server']['password'])
  80. ;
  81. }
  82. }
  83. // SMTP Configuration
  84. $smtpConfiguration = null;
  85. if (
  86. ! empty($params['smtp_server'])
  87. && !isset($params['smtp_server']['mailer_id'])
  88. ) {
  89. $smtpConfiguration = SMTP\Configuration::guessTransportDefinition($params['smtp_server']);
  90. if ($smtpConfiguration instanceof SMTP\Transport\AppTransportConfigurationInterface) {
  91. $smtpConfiguration
  92. ->setClient($params['smtp_server']['client'])
  93. ->setUsername($params['smtp_server']['username'])
  94. ;
  95. } else if ($smtpConfiguration instanceof SMTP\Transport\ResolvedTransportConfigurationInterface) {
  96. $smtpConfiguration
  97. ->setUsername($params['smtp_server']['username'])
  98. ->setPassword($params['smtp_server']['password'])
  99. ;
  100. } else {
  101. $smtpConfiguration
  102. ->setHost($params['smtp_server']['host'])
  103. ->setPort($params['smtp_server']['port'])
  104. ->setUsername($params['smtp_server']['username'])
  105. ->setPassword($params['smtp_server']['password'])
  106. ;
  107. if (! empty($params['smtp_server']['sender_address'])) {
  108. $smtpConfiguration
  109. ->setSenderAddress($params['smtp_server']['sender_address'])
  110. ;
  111. }
  112. }
  113. }
  114. // Mailbox Configuration
  115. ($mailbox = new Mailbox($id))
  116. ->setName($params['name'])
  117. ->setIsEnabled($params['enabled']);
  118. if (! empty($imapConfiguration)) {
  119. $mailbox
  120. ->setImapConfiguration($imapConfiguration)
  121. ;
  122. }
  123. if (! empty($smtpConfiguration)) {
  124. $mailbox
  125. ->setSmtpConfiguration($smtpConfiguration)
  126. ;
  127. }
  128. if (! empty($swiftMailerConfiguration)) {
  129. $mailbox->setSwiftMailerConfiguration($swiftMailerConfiguration);
  130. } else if (! empty($params['smtp_server']['mailer_id']) && true === $ignoreInvalidAttributes) {
  131. $mailbox->setSwiftMailerConfiguration($swiftmailerService->createConfiguration('smtp', $params['smtp_server']['mailer_id']));
  132. }
  133. $mailboxConfiguration->addMailbox($mailbox);
  134. }
  135. return $mailboxConfiguration;
  136. }
  137. private function getParser()
  138. {
  139. if (empty($this->parser)) {
  140. $this->parser = new EmailParser();
  141. }
  142. return $this->parser;
  143. }
  144. private function getLoadedEmailContentParser($emailContents = null, $cacheContent = true): ?EmailParser
  145. {
  146. if (empty($emailContents)) {
  147. return $this->emailParser ?? null;
  148. }
  149. $emailParser = new EmailParser();
  150. $emailParser
  151. ->setText($emailContents)
  152. ;
  153. if ($cacheContent) {
  154. $this->emailParser = $emailParser;
  155. }
  156. return $emailParser;
  157. }
  158. private function getRegisteredMailboxes()
  159. {
  160. if (empty($this->mailboxCollection)) {
  161. $this->mailboxCollection = array_map(function ($mailboxId) {
  162. return $this->container->getParameter("uvdesk.mailboxes.$mailboxId");
  163. }, $this->container->getParameter('uvdesk.mailboxes'));
  164. }
  165. return $this->mailboxCollection;
  166. }
  167. public function getRegisteredMailboxesById()
  168. {
  169. // Fetch existing content in file
  170. $filePath = $this->getPathToConfigurationFile();
  171. $file_content = file_get_contents($filePath);
  172. // Convert yaml file content into array and merge existing mailbox and new mailbox
  173. $file_content_array = Yaml::parse($file_content, 6);
  174. if ($file_content_array['uvdesk_mailbox']['mailboxes']) {
  175. foreach ($file_content_array['uvdesk_mailbox']['mailboxes'] as $key => $value) {
  176. $value['mailbox_id'] = $key;
  177. $mailboxCollection[] = $value;
  178. }
  179. }
  180. return $mailboxCollection ?? [];
  181. }
  182. public function getEmailAddresses($collection)
  183. {
  184. $formattedCollection = array_map(function ($emailAddress) {
  185. if (filter_var($emailAddress['address'], FILTER_VALIDATE_EMAIL)) {
  186. return $emailAddress['address'];
  187. }
  188. return null;
  189. }, (array) $collection);
  190. $filteredCollection = array_values(array_filter($formattedCollection));
  191. return count($filteredCollection) == 1 ? $filteredCollection[0] : $filteredCollection;
  192. }
  193. public function parseAddress($type)
  194. {
  195. $addresses = mailparse_rfc822_parse_addresses($this->getParser()->getHeader($type));
  196. return $addresses ?: false;
  197. }
  198. public function getEmailAddress($addresses)
  199. {
  200. foreach ((array) $addresses as $address) {
  201. if (filter_var($address['address'], FILTER_VALIDATE_EMAIL)) {
  202. return $address['address'];
  203. }
  204. }
  205. return null;
  206. }
  207. public function getMailboxByEmail($email)
  208. {
  209. foreach ($this->getRegisteredMailboxes() as $registeredMailbox) {
  210. if (strtolower($email) === strtolower($registeredMailbox['imap_server']['username'])) {
  211. return $registeredMailbox;
  212. }
  213. }
  214. throw new \Exception("No mailbox found for email '$email'");
  215. }
  216. public function getMailboxByToEmail($email)
  217. {
  218. foreach ($this->getRegisteredMailboxes() as $registeredMailbox) {
  219. if (strtolower($email) === strtolower($registeredMailbox['imap_server']['username'])) {
  220. return true;
  221. }
  222. }
  223. return false;
  224. }
  225. private function searchTicketSubjectReference($senderEmail, $messageSubject) {
  226. // Search Criteria: Find ticket based on subject
  227. if (
  228. ! empty($senderEmail)
  229. && ! empty($messageSubject)
  230. ) {
  231. $threadRepository = $this->entityManager->getRepository(Thread::class);
  232. $ticket = $threadRepository->findTicketBySubject($senderEmail, $messageSubject);
  233. if ($ticket != null) {
  234. return $ticket;
  235. }
  236. }
  237. return null;
  238. }
  239. private function searchExistingTickets(array $criterias = [])
  240. {
  241. if (empty($criterias)) {
  242. return null;
  243. }
  244. $ticketRepository = $this->entityManager->getRepository(Ticket::class);
  245. $threadRepository = $this->entityManager->getRepository(Thread::class);
  246. foreach ($criterias as $criteria => $criteriaValue) {
  247. if (empty($criteriaValue)) {
  248. continue;
  249. }
  250. switch ($criteria) {
  251. case 'messageId':
  252. // Search Criteria 1: Find ticket by unique message id
  253. $ticket = $ticketRepository->findOneByReferenceIds($criteriaValue);
  254. if (! empty($ticket)) {
  255. return $ticket;
  256. } else {
  257. $thread = $threadRepository->findOneByMessageId($criteriaValue);
  258. if (! empty($thread)) {
  259. return $thread->getTicket();
  260. }
  261. }
  262. break;
  263. case 'outlookConversationId':
  264. // Search Criteria 1: Find ticket by unique message id
  265. $ticket = $ticketRepository->findOneByOutlookConversationId($criteriaValue);
  266. if (! empty($ticket)) {
  267. return $ticket;
  268. }
  269. break;
  270. case 'inReplyTo':
  271. // Search Criteria 2: Find ticket based on in-reply-to reference id
  272. $ticket = $this->entityManager->getRepository(Thread::class)->findThreadByRefrenceId($criteriaValue);
  273. if (! empty($ticket)) {
  274. return $ticket;
  275. } else {
  276. $thread = $threadRepository->findOneByMessageId($criteriaValue);
  277. if (! empty($thread)) {
  278. return $thread->getTicket();
  279. }
  280. }
  281. break;
  282. case 'referenceIds':
  283. // Search Criteria 3: Find ticket based on reference id
  284. // Break references into ind. message id collection, and iteratively
  285. // search for existing threads for these message ids.
  286. $referenceIds = explode(' ', $criteriaValue);
  287. foreach ($referenceIds as $messageId) {
  288. $thread = $threadRepository->findOneByMessageId($messageId);
  289. if (! empty($thread)) {
  290. return $thread->getTicket();
  291. }
  292. }
  293. break;
  294. default:
  295. break;
  296. }
  297. }
  298. return null;
  299. }
  300. public function processMail($rawEmail)
  301. {
  302. $mailData = [];
  303. $parser = $this->getParser();
  304. $parser->setText($rawEmail);
  305. $from = $this->parseAddress('from') ?: $this->parseAddress('sender');
  306. $addresses = [
  307. 'from' => $this->getEmailAddress($from),
  308. 'to' => empty($this->parseAddress('X-Forwarded-To')) ? $this->parseAddress('to') : $this->parseAddress('X-Forwarded-To'),
  309. 'cc' => $this->parseAddress('cc'),
  310. 'delivered-to' => $this->parseAddress('delivered-to'),
  311. ];
  312. if (empty($addresses['from'])) {
  313. return [
  314. 'message' => "No 'from' email address was found while processing contents of email.",
  315. 'content' => [],
  316. ];
  317. } else {
  318. if (! empty($addresses['delivered-to'])) {
  319. $addresses['to'] = array_map(function($address) {
  320. return $address['address'];
  321. }, $addresses['delivered-to']);
  322. } else if (! empty($addresses['to'])) {
  323. $addresses['to'] = array_map(function($address) {
  324. return $address['address'];
  325. }, $addresses['to']);
  326. } else if (! empty($addresses['cc'])) {
  327. $addresses['to'] = array_map(function($address) {
  328. return $address['address'];
  329. }, $addresses['cc']);
  330. }
  331. // Skip email processing if no to-emails are specified
  332. if (empty($addresses['to'])) {
  333. return [
  334. 'message' => "No 'to' email addresses were found in the email.",
  335. 'content' => [
  336. 'from' => ! empty($addresses['from']) ? $addresses['from'] : null,
  337. ],
  338. ];
  339. }
  340. // Skip email processing if email is an auto-forwarded message to prevent infinite loop.
  341. if ($parser->getHeader('precedence') || $parser->getHeader('x-autoreply') || $parser->getHeader('x-autorespond') || 'auto-replied' == $parser->getHeader('auto-submitted')) {
  342. return [
  343. 'message' => "Received an auto-forwarded email which can lead to possible infinite loop of email exchanges. Skipping email from further processing.",
  344. 'content' => [
  345. 'from' => ! empty($addresses['from']) ? $addresses['from'] : null,
  346. ],
  347. ];
  348. }
  349. // Check for self-referencing. Skip email processing if a mailbox is configured by the sender's address.
  350. try {
  351. $this->getMailboxByEmail($addresses['from']);
  352. return [
  353. 'message' => "Received a self-referencing email where the sender email address matches one of the configured mailbox address. Skipping email from further processing.",
  354. 'content' => [
  355. 'from' => !empty($addresses['from']) ? $addresses['from'] : null,
  356. ],
  357. ];
  358. } catch (\Exception $e) {
  359. // An exception being thrown means no mailboxes were found from the recipient's address. Continue processing.
  360. }
  361. }
  362. $mailData['replyTo'] = '';
  363. foreach ($addresses['to'] as $mailboxEmail){
  364. if ($this->getMailboxByToEmail(strtolower($mailboxEmail))) {
  365. $mailData['replyTo'] = $mailboxEmail;
  366. }
  367. }
  368. // Process Mail - References
  369. $addresses['to'][0] = isset($mailData['replyTo']) ? strtolower($mailData['replyTo']) : strtolower($addresses['to'][0]);
  370. $mailData['replyTo'] = $addresses['to'];
  371. $mailData['messageId'] = $parser->getHeader('message-id') ?: null;
  372. $mailData['inReplyTo'] = htmlspecialchars_decode($parser->getHeader('in-reply-to'));
  373. $mailData['referenceIds'] = htmlspecialchars_decode($parser->getHeader('references'));
  374. $mailData['cc'] = array_filter(explode(',', $parser->getHeader('cc'))) ?: [];
  375. $mailData['bcc'] = array_filter(explode(',', $parser->getHeader('bcc'))) ?: [];
  376. // Process Mail - User Details
  377. $mailData['source'] = 'email';
  378. $mailData['createdBy'] = 'customer';
  379. $mailData['role'] = 'ROLE_CUSTOMER';
  380. $mailData['from'] = $addresses['from'];
  381. $mailData['name'] = trim(current(explode('@', $from[0]['display'])));
  382. // Process Mail - Content
  383. try {
  384. $htmlFilter = new HTMLFilter();
  385. $mailData['subject'] = $parser->getHeader('subject');
  386. $mailData['message'] = autolink($htmlFilter->addClassEmailReplyQuote($parser->getMessageBody('htmlEmbedded')));
  387. $mailData['attachments'] = $parser->getAttachments();
  388. } catch(\Exception $e) {
  389. return [
  390. 'error' => true,
  391. 'message' => $e->getMessage(),
  392. ];
  393. }
  394. if (! $mailData['message']) {
  395. $mailData['message'] = autolink($htmlFilter->addClassEmailReplyQuote($parser->getMessageBody('text')));
  396. }
  397. $website = $this->entityManager->getRepository(Website::class)->findOneByCode('knowledgebase');
  398. if (! empty($mailData['from']) && $this->container->get('ticket.service')->isEmailBlocked($mailData['from'], $website)) {
  399. return [
  400. 'message' => "Received email where the sender email address is present in the block list. Skipping this email from further processing.",
  401. 'content' => [
  402. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  403. ],
  404. ];
  405. }
  406. // Search for any existing tickets
  407. $ticket = $this->searchExistingTickets([
  408. 'messageId' => $mailData['messageId'],
  409. 'inReplyTo' => $mailData['inReplyTo'],
  410. 'referenceIds' => $mailData['referenceIds'],
  411. 'from' => $mailData['from'],
  412. 'subject' => $mailData['subject'],
  413. ]);
  414. if (empty($ticket)) {
  415. $mailData['threadType'] = 'create';
  416. $mailData['referenceIds'] = $mailData['messageId'];
  417. // @Todo For same subject with same customer check
  418. // $ticketSubjectReferenceExist = $this->searchTicketSubjectReference($mailData['from'], $mailData['subject']);
  419. // if (!empty($ticketSubjectReferenceExist)) {
  420. // return;
  421. // }
  422. $thread = $this->container->get('ticket.service')->createTicket($mailData);
  423. // Trigger ticket created event
  424. $event = new CoreWorkflowEvents\Ticket\Create();
  425. $event
  426. ->setTicket($thread->getTicket())
  427. ;
  428. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  429. } else if (false === $ticket->getIsTrashed() && strtolower($ticket->getStatus()->getCode()) != 'spam' && !empty($mailData['inReplyTo'])) {
  430. $mailData['threadType'] = 'reply';
  431. $thread = $this->entityManager->getRepository(Thread::class)->findOneByMessageId($mailData['messageId']);
  432. $ticketRef = $this->entityManager->getRepository(Ticket::class)->findById($ticket->getId());
  433. $referenceIds = explode(' ', $ticketRef[0]->getReferenceIds());
  434. if (!empty($thread)) {
  435. // Thread with the same message id exists skip process.
  436. return [
  437. 'message' => "The contents of this email has already been processed.",
  438. 'content' => [
  439. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  440. 'thread' => $thread->getId(),
  441. 'ticket' => $ticket->getId(),
  442. ],
  443. ];
  444. }
  445. if (in_array($mailData['messageId'], $referenceIds)) {
  446. // Thread with the same message id exists skip process.
  447. return [
  448. 'message' => "The contents of this email has already been processed.",
  449. 'content' => [
  450. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  451. ],
  452. ];
  453. }
  454. if (
  455. $ticket->getCustomer()
  456. && $ticket->getCustomer()->getEmail() == $mailData['from']
  457. ) {
  458. // Reply from customer
  459. $user = $ticket->getCustomer();
  460. $mailData['user'] = $user;
  461. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  462. } else if ($this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])) {
  463. // Reply from collaborator
  464. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  465. $mailData['user'] = $user;
  466. $mailData['createdBy'] = 'collaborator';
  467. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  468. } else {
  469. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  470. if (
  471. ! empty($user)
  472. && null != $user->getAgentInstance()
  473. ) {
  474. $mailData['user'] = $user;
  475. $mailData['createdBy'] = 'agent';
  476. $userDetails = $user->getAgentInstance()->getPartialDetails();
  477. } else {
  478. // Add user as a ticket collaborator
  479. if (empty($user)) {
  480. // Create a new user instance with customer support role
  481. $role = $this->entityManager->getRepository(SupportRole::class)->findOneByCode('ROLE_CUSTOMER');
  482. $user = $this->container->get('user.service')->createUserInstance($mailData['from'], $mailData['name'], $role, [
  483. 'source' => 'email',
  484. 'active' => true
  485. ]);
  486. }
  487. $mailData['user'] = $user;
  488. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  489. if (false == $this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])) {
  490. $ticket->addCollaborator($user);
  491. $this->entityManager->persist($ticket);
  492. $this->entityManager->flush();
  493. $ticket->lastCollaborator = $user;
  494. $event = new CoreWorkflowEvents\Ticket\Collaborator();
  495. $event
  496. ->setTicket($ticket)
  497. ;
  498. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  499. }
  500. }
  501. }
  502. $mailData['fullname'] = $userDetails['name'];
  503. $thread = $this->container->get('ticket.service')->createThread($ticket, $mailData);
  504. if ($thread->getThreadType() == 'reply') {
  505. if ($thread->getCreatedBy() == 'customer') {
  506. $event = new CoreWorkflowEvents\Ticket\CustomerReply();
  507. $event
  508. ->setTicket($ticket)
  509. ;
  510. } else if ($thread->getCreatedBy() == 'collaborator') {
  511. $event = new CoreWorkflowEvents\Ticket\CollaboratorReply();
  512. $event
  513. ->setTicket($ticket)
  514. ;
  515. } else {
  516. $event = new CoreWorkflowEvents\Ticket\AgentReply();
  517. $event
  518. ->setTicket($ticket)
  519. ;
  520. }
  521. }
  522. // Trigger thread reply event
  523. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  524. } else if (false === $ticket->getIsTrashed() && strtolower($ticket->getStatus()->getCode()) != 'spam' && empty($mailData['inReplyTo'])) {
  525. return [
  526. 'message' => "The contents of this email has already been processed.",
  527. 'content' => [
  528. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  529. 'thread' => ! empty($thread) ? $thread->getId() : null,
  530. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  531. ],
  532. ];
  533. }
  534. return [
  535. 'message' => "Inbound email processed successfully.",
  536. 'content' => [
  537. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  538. 'thread' => ! empty($thread) ? $thread->getId() : null,
  539. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  540. ],
  541. ];
  542. }
  543. public function processOutlookMail(array $outlookEmail)
  544. {
  545. $mailData = [];
  546. $senderName = null;
  547. $senderAddress = null;
  548. if (! empty($outlookEmail['from']['emailAddress']['address'])) {
  549. $senderName = $outlookEmail['from']['emailAddress']['name'];
  550. $senderAddress = $outlookEmail['from']['emailAddress']['address'];
  551. } else if (! empty($outlookEmail['sender']['emailAddress']['address'])) {
  552. $senderName = $outlookEmail['sender']['emailAddress']['name'];
  553. $senderAddress = $outlookEmail['sender']['emailAddress']['address'];
  554. } else {
  555. return [
  556. 'message' => "No 'from' email address was found while processing contents of email.",
  557. 'content' => [],
  558. ];
  559. }
  560. $toRecipients = array_map(function ($recipient) { return $recipient['emailAddress']['address']; }, $outlookEmail['toRecipients']);
  561. $ccRecipients = array_map(function ($recipient) { return $recipient['emailAddress']['address']; }, $outlookEmail['ccRecipients'] ?? []);
  562. $bccRecipients = array_map(function ($recipient) { return $recipient['emailAddress']['address']; }, $outlookEmail['bccRecipients'] ?? []);
  563. $addresses = [
  564. 'from' => $senderAddress,
  565. 'to' => $toRecipients,
  566. 'cc' => $ccRecipients,
  567. ];
  568. // Skip email processing if no to-emails are specified
  569. if (empty($addresses['to'])) {
  570. return [
  571. 'message' => "No 'to' email addresses were found in the email.",
  572. 'content' => [
  573. 'from' => $senderAddress ?? null,
  574. ],
  575. ];
  576. }
  577. // Check for self-referencing. Skip email processing if a mailbox is configured by the sender's address.
  578. try {
  579. $this->getMailboxByEmail($senderAddress);
  580. return [
  581. 'message' => "Received a self-referencing email where the sender email address matches one of the configured mailbox address. Skipping email from further processing.",
  582. 'content' => [
  583. 'from' => $senderAddress ?? null,
  584. ],
  585. ];
  586. } catch (\Exception $e) {
  587. // An exception being thrown means no mailboxes were found from the recipient's address. Continue processing.
  588. }
  589. // Process Mail - References
  590. // $addresses['to'][0] = isset($mailData['replyTo']) ? strtolower($mailData['replyTo']) : strtolower($addresses['to'][0]);
  591. $mailData['replyTo'] = $addresses['to'];
  592. $mailData['messageId'] = $outlookEmail['internetMessageId'];
  593. $mailData['outlookConversationId'] = $outlookEmail['conversationId'];
  594. $mailData['inReplyTo'] = $outlookEmail['conversationId'];
  595. // $mailData['inReplyTo'] = htmlspecialchars_decode($parser->getHeader('in-reply-to'));
  596. $mailData['referenceIds'] = '';
  597. // $mailData['referenceIds'] = htmlspecialchars_decode($parser->getHeader('references'));
  598. $mailData['cc'] = $ccRecipients;
  599. $mailData['bcc'] = $bccRecipients;
  600. // Process Mail - User Details
  601. $mailData['source'] = 'email';
  602. $mailData['createdBy'] = 'customer';
  603. $mailData['role'] = 'ROLE_CUSTOMER';
  604. $mailData['from'] = $senderAddress;
  605. $mailData['name'] = trim($senderName);
  606. // Process Mail - Content
  607. $htmlFilter = new HTMLFilter();
  608. $mailData['subject'] = $outlookEmail['subject'];
  609. $mailData['message'] = autolink($htmlFilter->addClassEmailReplyQuote($outlookEmail['body']['content']));
  610. $mailData['attachments'] = [];
  611. $mailData['attachmentContent'] = isset($outlookEmail['outlookAttachments']) ? $outlookEmail['outlookAttachments'] : [];
  612. $website = $this->entityManager->getRepository(Website::class)->findOneByCode('knowledgebase');
  613. if (
  614. ! empty($mailData['from'])
  615. && $this->container->get('ticket.service')->isEmailBlocked($mailData['from'], $website)
  616. ) {
  617. return [
  618. 'message' => "Received email where the sender email address is present in the block list. Skipping this email from further processing.",
  619. 'content' => [
  620. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  621. ],
  622. ];
  623. }
  624. // return [
  625. // 'outlookConversationId' => $mailData['outlookConversationId'],
  626. // 'message' => "No 'to' email addresses were found in the email.",
  627. // 'content' => [
  628. // 'outlookConversationId' => $mailData['outlookConversationId'],
  629. // ],
  630. // ];
  631. // Search for any existing tickets
  632. $ticket = $this->searchExistingTickets([
  633. 'messageId' => $mailData['messageId'],
  634. 'inReplyTo' => $mailData['inReplyTo'],
  635. 'referenceIds' => $mailData['referenceIds'],
  636. 'from' => $mailData['from'],
  637. 'subject' => $mailData['subject'],
  638. 'outlookConversationId' => $mailData['outlookConversationId'],
  639. ]);
  640. if (empty($ticket)) {
  641. $mailData['threadType'] = 'create';
  642. $mailData['referenceIds'] = $mailData['messageId'];
  643. // @Todo For same subject with same customer check
  644. // $ticketSubjectReferenceExist = $this->searchTicketSubjectReference($mailData['from'], $mailData['subject']);
  645. // if(!empty($ticketSubjectReferenceExist)) {
  646. // return;
  647. // }
  648. $thread = $this->container->get('ticket.service')->createTicket($mailData);
  649. // Trigger ticket created event
  650. $event = new CoreWorkflowEvents\Ticket\Create();
  651. $event
  652. ->setTicket($thread->getTicket())
  653. ;
  654. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  655. } else if (
  656. false === $ticket->getIsTrashed()
  657. && strtolower($ticket->getStatus()->getCode()) != 'spam'
  658. && ! empty($mailData['inReplyTo'])
  659. ) {
  660. $mailData['threadType'] = 'reply';
  661. $thread = $this->entityManager->getRepository(Thread::class)->findOneByMessageId($mailData['messageId']);
  662. $ticketRef = $this->entityManager->getRepository(Ticket::class)->findById($ticket->getId());
  663. $referenceIds = explode(' ', $ticketRef[0]->getReferenceIds());
  664. if (! empty($thread)) {
  665. // Thread with the same message id exists skip process.
  666. return [
  667. 'message' => "The contents of this email has already been processed 1.",
  668. 'content' => [
  669. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  670. 'thread' => $thread->getId(),
  671. 'ticket' => $ticket->getId(),
  672. ],
  673. ];
  674. }
  675. if (in_array($mailData['messageId'], $referenceIds)) {
  676. // Thread with the same message id exists skip process.
  677. return [
  678. 'message' => "The contents of this email has already been processed 2.",
  679. 'content' => [
  680. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  681. ],
  682. ];
  683. }
  684. if ($ticket->getCustomer() && $ticket->getCustomer()->getEmail() == $mailData['from']) {
  685. // Reply from customer
  686. $user = $ticket->getCustomer();
  687. $mailData['user'] = $user;
  688. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  689. } else if ($this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])){
  690. // Reply from collaborator
  691. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  692. $mailData['user'] = $user;
  693. $mailData['createdBy'] = 'collaborator';
  694. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  695. } else {
  696. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  697. if (! empty($user) && null != $user->getAgentInstance()) {
  698. $mailData['user'] = $user;
  699. $mailData['createdBy'] = 'agent';
  700. $userDetails = $user->getAgentInstance()->getPartialDetails();
  701. } else {
  702. // Add user as a ticket collaborator
  703. if (empty($user)) {
  704. // Create a new user instance with customer support role
  705. $role = $this->entityManager->getRepository(SupportRole::class)->findOneByCode('ROLE_CUSTOMER');
  706. $user = $this->container->get('user.service')->createUserInstance($mailData['from'], $mailData['name'], $role, [
  707. 'source' => 'email',
  708. 'active' => true
  709. ]);
  710. }
  711. $mailData['user'] = $user;
  712. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  713. if (false == $this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])) {
  714. $ticket->addCollaborator($user);
  715. $this->entityManager->persist($ticket);
  716. $this->entityManager->flush();
  717. $ticket->lastCollaborator = $user;
  718. $event = new CoreWorkflowEvents\Ticket\Collaborator();
  719. $event
  720. ->setTicket($ticket)
  721. ;
  722. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  723. }
  724. }
  725. }
  726. $mailData['fullname'] = $userDetails['name'];
  727. $thread = $this->container->get('ticket.service')->createThread($ticket, $mailData);
  728. if ($thread->getThreadType() == 'reply') {
  729. if ($thread->getCreatedBy() == 'customer') {
  730. $event = new CoreWorkflowEvents\Ticket\CustomerReply();
  731. $event
  732. ->setTicket($ticket)
  733. ;
  734. } else if ($thread->getCreatedBy() == 'collaborator') {
  735. $event = new CoreWorkflowEvents\Ticket\CollaboratorReply();
  736. $event
  737. ->setTicket($ticket)
  738. ;
  739. } else {
  740. $event = new CoreWorkflowEvents\Ticket\AgentReply();
  741. $event
  742. ->setTicket($ticket)
  743. ;
  744. }
  745. }
  746. // Trigger thread reply event
  747. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  748. } else if (false === $ticket->getIsTrashed() && strtolower($ticket->getStatus()->getCode()) != 'spam' && empty($mailData['inReplyTo'])) {
  749. return [
  750. 'message' => "The contents of this email has already been processed 3.",
  751. 'content' => [
  752. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  753. 'thread' => ! empty($thread) ? $thread->getId() : null,
  754. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  755. ],
  756. ];
  757. }
  758. return [
  759. 'message' => "Inbound email processed successfully.",
  760. 'content' => [
  761. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  762. 'thread' => ! empty($thread) ? $thread->getId() : null,
  763. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  764. ],
  765. ];
  766. }
  767. }