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. //Fix when the sender is noreply try to parse email and name from message body
  388. if (preg_match('/^(noreply|support)@(?:gglot\.com|doctranslator\.com|doc-translator\.com|conveythis\.com|textflip\.ai|help-desk\.ai)$/', $mailData['from'])) {
  389. if (preg_match('/^email:\s*(.+)\s*(?:<br>|$)/Umsi', $mailData['message'], $matches)) {
  390. $mailData['from'] = trim($matches[1]);
  391. }
  392. if (preg_match('/<p>Email:\s*<strong>([^<]+)<\/strong><\/p>/i', $mailData['message'], $matches)) {
  393. $mailData['from'] = trim($matches[1]);
  394. }
  395. if (preg_match('/From:\s*([^<]+)<([^>]+)>/i', $mailData['message'], $matches)) {
  396. $mailData['name'] = trim($matches[1]);
  397. }
  398. if (preg_match('/(?:^|>)name:\s*(.+)\s*<br>/Umsi', $mailData['message'], $matches)) {
  399. $mailData['name'] = trim($matches[1]);
  400. }
  401. if (preg_match('/Name:\s*<strong>([^<]+)<\/strong>/i', $mailData['message'], $matches)) {
  402. $mailData['name'] = trim($matches[1]);
  403. }
  404. if (preg_match('/(?:^|>)topic:\s*([^<]+)\s*<\s*\/?br\s*\/?>/Umsi', $mailData['message'], $matches)) {
  405. if (strtolower(trim($matches[1])) != 'not mentioned') {
  406. $mailData['subject'] = trim($matches[1]);
  407. }
  408. }
  409. }
  410. // Process Mail - Content
  411. try {
  412. $htmlFilter = new HTMLFilter();
  413. $mailData['subject'] = $parser->getHeader('subject');
  414. $mailData['message'] = autolink($htmlFilter->addClassEmailReplyQuote($parser->getMessageBody('htmlEmbedded')));
  415. $mailData['attachments'] = $parser->getAttachments();
  416. } catch(\Exception $e) {
  417. return [
  418. 'error' => true,
  419. 'message' => $e->getMessage(),
  420. ];
  421. }
  422. if (! $mailData['message']) {
  423. $mailData['message'] = autolink($htmlFilter->addClassEmailReplyQuote($parser->getMessageBody('text')));
  424. }
  425. $website = $this->entityManager->getRepository(Website::class)->findOneByCode('knowledgebase');
  426. if (! empty($mailData['from']) && $this->container->get('ticket.service')->isEmailBlocked($mailData['from'], $website)) {
  427. return [
  428. 'message' => "Received email where the sender email address is present in the block list. Skipping this email from further processing.",
  429. 'content' => [
  430. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  431. ],
  432. ];
  433. }
  434. // Search for any existing tickets
  435. $ticket = $this->searchExistingTickets([
  436. 'messageId' => $mailData['messageId'],
  437. 'inReplyTo' => $mailData['inReplyTo'],
  438. 'referenceIds' => $mailData['referenceIds'],
  439. 'from' => $mailData['from'],
  440. 'subject' => $mailData['subject'],
  441. ]);
  442. if (empty($ticket)) {
  443. $mailData['threadType'] = 'create';
  444. $mailData['referenceIds'] = $mailData['messageId'];
  445. // @Todo For same subject with same customer check
  446. // $ticketSubjectReferenceExist = $this->searchTicketSubjectReference($mailData['from'], $mailData['subject']);
  447. // if (!empty($ticketSubjectReferenceExist)) {
  448. // return;
  449. // }
  450. $thread = $this->container->get('ticket.service')->createTicket($mailData);
  451. // Trigger ticket created event
  452. $this->print_log('ticket created event of ' . $mailData['messageId']);
  453. $event = new CoreWorkflowEvents\Ticket\Create();
  454. $event
  455. ->setTicket($thread->getTicket())
  456. ;
  457. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  458. } else if (false === $ticket->getIsTrashed() && strtolower($ticket->getStatus()->getCode()) != 'spam' && !empty($mailData['inReplyTo'])) {
  459. $mailData['threadType'] = 'reply';
  460. $thread = $this->entityManager->getRepository(Thread::class)->findOneByMessageId($mailData['messageId']);
  461. $ticketRef = $this->entityManager->getRepository(Ticket::class)->findById($ticket->getId());
  462. $referenceIds = explode(' ', $ticketRef[0]->getReferenceIds());
  463. if (!empty($thread)) {
  464. // Thread with the same message id exists skip process.
  465. return [
  466. 'message' => "The contents of this email has already been processed.",
  467. 'content' => [
  468. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  469. 'thread' => $thread->getId(),
  470. 'ticket' => $ticket->getId(),
  471. ],
  472. ];
  473. }
  474. if (in_array($mailData['messageId'], $referenceIds)) {
  475. // Thread with the same message id exists skip process.
  476. return [
  477. 'message' => "The contents of this email has already been processed.",
  478. 'content' => [
  479. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  480. ],
  481. ];
  482. }
  483. if (
  484. $ticket->getCustomer()
  485. && $ticket->getCustomer()->getEmail() == $mailData['from']
  486. ) {
  487. // Reply from customer
  488. $user = $ticket->getCustomer();
  489. $mailData['user'] = $user;
  490. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  491. } else if ($this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])) {
  492. // Reply from collaborator
  493. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  494. $mailData['user'] = $user;
  495. $mailData['createdBy'] = 'collaborator';
  496. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  497. } else {
  498. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  499. if (
  500. ! empty($user)
  501. && null != $user->getAgentInstance()
  502. ) {
  503. $mailData['user'] = $user;
  504. $mailData['createdBy'] = 'agent';
  505. $userDetails = $user->getAgentInstance()->getPartialDetails();
  506. } else {
  507. // Add user as a ticket collaborator
  508. if (empty($user)) {
  509. // Create a new user instance with customer support role
  510. $role = $this->entityManager->getRepository(SupportRole::class)->findOneByCode('ROLE_CUSTOMER');
  511. $user = $this->container->get('user.service')->createUserInstance($mailData['from'], $mailData['name'], $role, [
  512. 'source' => 'email',
  513. 'active' => true
  514. ]);
  515. }
  516. $mailData['user'] = $user;
  517. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  518. if (false == $this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])) {
  519. $ticket->addCollaborator($user);
  520. $this->entityManager->persist($ticket);
  521. $this->entityManager->flush();
  522. $ticket->lastCollaborator = $user;
  523. $event = new CoreWorkflowEvents\Ticket\Collaborator();
  524. $event
  525. ->setTicket($ticket)
  526. ;
  527. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  528. }
  529. }
  530. }
  531. $mailData['fullname'] = $userDetails['name'];
  532. $thread = $this->container->get('ticket.service')->createThread($ticket, $mailData);
  533. if ($thread->getThreadType() == 'reply') {
  534. if ($thread->getCreatedBy() == 'customer') {
  535. $event = new CoreWorkflowEvents\Ticket\CustomerReply();
  536. $event
  537. ->setTicket($ticket)
  538. ;
  539. } else if ($thread->getCreatedBy() == 'collaborator') {
  540. $event = new CoreWorkflowEvents\Ticket\CollaboratorReply();
  541. $event
  542. ->setTicket($ticket)
  543. ;
  544. } else {
  545. $event = new CoreWorkflowEvents\Ticket\AgentReply();
  546. $event
  547. ->setTicket($ticket)
  548. ;
  549. }
  550. }
  551. // Trigger thread reply event
  552. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  553. } else if (false === $ticket->getIsTrashed() && strtolower($ticket->getStatus()->getCode()) != 'spam' && empty($mailData['inReplyTo'])) {
  554. return [
  555. 'message' => "The contents of this email has already been processed.",
  556. 'content' => [
  557. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  558. 'thread' => ! empty($thread) ? $thread->getId() : null,
  559. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  560. ],
  561. ];
  562. }
  563. return [
  564. 'message' => "Inbound email processed successfully.",
  565. 'content' => [
  566. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  567. 'thread' => ! empty($thread) ? $thread->getId() : null,
  568. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  569. ],
  570. ];
  571. }
  572. public function processOutlookMail(array $outlookEmail)
  573. {
  574. $mailData = [];
  575. $senderName = null;
  576. $senderAddress = null;
  577. if (! empty($outlookEmail['from']['emailAddress']['address'])) {
  578. $senderName = $outlookEmail['from']['emailAddress']['name'];
  579. $senderAddress = $outlookEmail['from']['emailAddress']['address'];
  580. } else if (! empty($outlookEmail['sender']['emailAddress']['address'])) {
  581. $senderName = $outlookEmail['sender']['emailAddress']['name'];
  582. $senderAddress = $outlookEmail['sender']['emailAddress']['address'];
  583. } else {
  584. return [
  585. 'message' => "No 'from' email address was found while processing contents of email.",
  586. 'content' => [],
  587. ];
  588. }
  589. $toRecipients = array_map(function ($recipient) { return $recipient['emailAddress']['address']; }, $outlookEmail['toRecipients']);
  590. $ccRecipients = array_map(function ($recipient) { return $recipient['emailAddress']['address']; }, $outlookEmail['ccRecipients'] ?? []);
  591. $bccRecipients = array_map(function ($recipient) { return $recipient['emailAddress']['address']; }, $outlookEmail['bccRecipients'] ?? []);
  592. $addresses = [
  593. 'from' => $senderAddress,
  594. 'to' => $toRecipients,
  595. 'cc' => $ccRecipients,
  596. ];
  597. // Skip email processing if no to-emails are specified
  598. if (empty($addresses['to'])) {
  599. return [
  600. 'message' => "No 'to' email addresses were found in the email.",
  601. 'content' => [
  602. 'from' => $senderAddress ?? null,
  603. ],
  604. ];
  605. }
  606. // Check for self-referencing. Skip email processing if a mailbox is configured by the sender's address.
  607. try {
  608. $this->getMailboxByEmail($senderAddress);
  609. return [
  610. 'message' => "Received a self-referencing email where the sender email address matches one of the configured mailbox address. Skipping email from further processing.",
  611. 'content' => [
  612. 'from' => $senderAddress ?? null,
  613. ],
  614. ];
  615. } catch (\Exception $e) {
  616. // An exception being thrown means no mailboxes were found from the recipient's address. Continue processing.
  617. }
  618. // Process Mail - References
  619. // $addresses['to'][0] = isset($mailData['replyTo']) ? strtolower($mailData['replyTo']) : strtolower($addresses['to'][0]);
  620. $mailData['replyTo'] = $addresses['to'];
  621. $mailData['messageId'] = $outlookEmail['internetMessageId'];
  622. $mailData['outlookConversationId'] = $outlookEmail['conversationId'];
  623. $mailData['inReplyTo'] = $outlookEmail['conversationId'];
  624. // $mailData['inReplyTo'] = htmlspecialchars_decode($parser->getHeader('in-reply-to'));
  625. $mailData['referenceIds'] = '';
  626. // $mailData['referenceIds'] = htmlspecialchars_decode($parser->getHeader('references'));
  627. $mailData['cc'] = $ccRecipients;
  628. $mailData['bcc'] = $bccRecipients;
  629. // Process Mail - User Details
  630. $mailData['source'] = 'email';
  631. $mailData['createdBy'] = 'customer';
  632. $mailData['role'] = 'ROLE_CUSTOMER';
  633. $mailData['from'] = $senderAddress;
  634. $mailData['name'] = trim($senderName);
  635. // Process Mail - Content
  636. $htmlFilter = new HTMLFilter();
  637. $mailData['subject'] = $outlookEmail['subject'];
  638. $mailData['message'] = autolink($htmlFilter->addClassEmailReplyQuote($outlookEmail['body']['content']));
  639. $mailData['attachments'] = [];
  640. $mailData['attachmentContent'] = isset($outlookEmail['outlookAttachments']) ? $outlookEmail['outlookAttachments'] : [];
  641. $website = $this->entityManager->getRepository(Website::class)->findOneByCode('knowledgebase');
  642. if (
  643. ! empty($mailData['from'])
  644. && $this->container->get('ticket.service')->isEmailBlocked($mailData['from'], $website)
  645. ) {
  646. return [
  647. 'message' => "Received email where the sender email address is present in the block list. Skipping this email from further processing.",
  648. 'content' => [
  649. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  650. ],
  651. ];
  652. }
  653. // return [
  654. // 'outlookConversationId' => $mailData['outlookConversationId'],
  655. // 'message' => "No 'to' email addresses were found in the email.",
  656. // 'content' => [
  657. // 'outlookConversationId' => $mailData['outlookConversationId'],
  658. // ],
  659. // ];
  660. // Search for any existing tickets
  661. $ticket = $this->searchExistingTickets([
  662. 'messageId' => $mailData['messageId'],
  663. 'inReplyTo' => $mailData['inReplyTo'],
  664. 'referenceIds' => $mailData['referenceIds'],
  665. 'from' => $mailData['from'],
  666. 'subject' => $mailData['subject'],
  667. 'outlookConversationId' => $mailData['outlookConversationId'],
  668. ]);
  669. if (empty($ticket)) {
  670. $mailData['threadType'] = 'create';
  671. $mailData['referenceIds'] = $mailData['messageId'];
  672. // @Todo For same subject with same customer check
  673. // $ticketSubjectReferenceExist = $this->searchTicketSubjectReference($mailData['from'], $mailData['subject']);
  674. // if(!empty($ticketSubjectReferenceExist)) {
  675. // return;
  676. // }
  677. $thread = $this->container->get('ticket.service')->createTicket($mailData);
  678. // Trigger ticket created event
  679. $event = new CoreWorkflowEvents\Ticket\Create();
  680. $event
  681. ->setTicket($thread->getTicket())
  682. ;
  683. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  684. } else if (
  685. false === $ticket->getIsTrashed()
  686. && strtolower($ticket->getStatus()->getCode()) != 'spam'
  687. && ! empty($mailData['inReplyTo'])
  688. ) {
  689. $mailData['threadType'] = 'reply';
  690. $thread = $this->entityManager->getRepository(Thread::class)->findOneByMessageId($mailData['messageId']);
  691. $ticketRef = $this->entityManager->getRepository(Ticket::class)->findById($ticket->getId());
  692. $referenceIds = explode(' ', $ticketRef[0]->getReferenceIds());
  693. if (! empty($thread)) {
  694. // Thread with the same message id exists skip process.
  695. return [
  696. 'message' => "The contents of this email has already been processed 1.",
  697. 'content' => [
  698. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  699. 'thread' => $thread->getId(),
  700. 'ticket' => $ticket->getId(),
  701. ],
  702. ];
  703. }
  704. if (in_array($mailData['messageId'], $referenceIds)) {
  705. // Thread with the same message id exists skip process.
  706. return [
  707. 'message' => "The contents of this email has already been processed 2.",
  708. 'content' => [
  709. 'from' => !empty($mailData['from']) ? $mailData['from'] : null,
  710. ],
  711. ];
  712. }
  713. if ($ticket->getCustomer() && $ticket->getCustomer()->getEmail() == $mailData['from']) {
  714. // Reply from customer
  715. $user = $ticket->getCustomer();
  716. $mailData['user'] = $user;
  717. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  718. } else if ($this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])){
  719. // Reply from collaborator
  720. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  721. $mailData['user'] = $user;
  722. $mailData['createdBy'] = 'collaborator';
  723. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  724. } else {
  725. $user = $this->entityManager->getRepository(User::class)->findOneByEmail($mailData['from']);
  726. if (! empty($user) && null != $user->getAgentInstance()) {
  727. $mailData['user'] = $user;
  728. $mailData['createdBy'] = 'agent';
  729. $userDetails = $user->getAgentInstance()->getPartialDetails();
  730. } else {
  731. // Add user as a ticket collaborator
  732. if (empty($user)) {
  733. // Create a new user instance with customer support role
  734. $role = $this->entityManager->getRepository(SupportRole::class)->findOneByCode('ROLE_CUSTOMER');
  735. $user = $this->container->get('user.service')->createUserInstance($mailData['from'], $mailData['name'], $role, [
  736. 'source' => 'email',
  737. 'active' => true
  738. ]);
  739. }
  740. $mailData['user'] = $user;
  741. $userDetails = $user->getCustomerInstance()->getPartialDetails();
  742. if (false == $this->entityManager->getRepository(Ticket::class)->isTicketCollaborator($ticket, $mailData['from'])) {
  743. $ticket->addCollaborator($user);
  744. $this->entityManager->persist($ticket);
  745. $this->entityManager->flush();
  746. $ticket->lastCollaborator = $user;
  747. $event = new CoreWorkflowEvents\Ticket\Collaborator();
  748. $event
  749. ->setTicket($ticket)
  750. ;
  751. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  752. }
  753. }
  754. }
  755. $mailData['fullname'] = $userDetails['name'];
  756. $thread = $this->container->get('ticket.service')->createThread($ticket, $mailData);
  757. if ($thread->getThreadType() == 'reply') {
  758. if ($thread->getCreatedBy() == 'customer') {
  759. $event = new CoreWorkflowEvents\Ticket\CustomerReply();
  760. $event
  761. ->setTicket($ticket)
  762. ;
  763. } else if ($thread->getCreatedBy() == 'collaborator') {
  764. $event = new CoreWorkflowEvents\Ticket\CollaboratorReply();
  765. $event
  766. ->setTicket($ticket)
  767. ;
  768. } else {
  769. $event = new CoreWorkflowEvents\Ticket\AgentReply();
  770. $event
  771. ->setTicket($ticket)
  772. ;
  773. }
  774. }
  775. // Trigger thread reply event
  776. $this->container->get('event_dispatcher')->dispatch($event, 'uvdesk.automation.workflow.execute');
  777. } else if (false === $ticket->getIsTrashed() && strtolower($ticket->getStatus()->getCode()) != 'spam' && empty($mailData['inReplyTo'])) {
  778. return [
  779. 'message' => "The contents of this email has already been processed 3.",
  780. 'content' => [
  781. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  782. 'thread' => ! empty($thread) ? $thread->getId() : null,
  783. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  784. ],
  785. ];
  786. }
  787. return [
  788. 'message' => "Inbound email processed successfully.",
  789. 'content' => [
  790. 'from' => ! empty($mailData['from']) ? $mailData['from'] : null,
  791. 'thread' => ! empty($thread) ? $thread->getId() : null,
  792. 'ticket' => ! empty($ticket) ? $ticket->getId() : null,
  793. ],
  794. ];
  795. }
  796. function print_log($msg) {
  797. $logFile = '/var/www/support-test.conveythis.com/print_mailbox.log';
  798. // If file exists and is bigger than 25 MB, clear it
  799. if (file_exists($logFile) && filesize($logFile) > 25 * 1024 * 1024) {
  800. file_put_contents($logFile, '');
  801. }
  802. $message = "[" . date('Y-m-d H:i:s') . "] " . $msg . "\n";
  803. file_put_contents($logFile, $message, FILE_APPEND | LOCK_EX);
  804. }
  805. }