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

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