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

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