You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MailFetcher.php 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <?php
  2. namespace App\Service;
  3. use PhpImap\IncomingMail;
  4. use PhpImap\IncomingMailAttachment;
  5. use Psr\Log\LoggerInterface;
  6. use SecIT\ImapBundle\Connection\ConnectionInterface;
  7. use Symfony\Component\DependencyInjection\Attribute\Target;
  8. use Symfony\Component\Filesystem\Filesystem;
  9. use Symfony\Component\Filesystem\Path;
  10. use App\Presentation\PortfolioPresenter as PresentationPortfolio;
  11. use App\Service\TelegramNotifier;
  12. readonly class MailFetcher
  13. {
  14. public function __construct(
  15. #[Target('finfollowConnection')]
  16. private ConnectionInterface $connection,
  17. private XmlParser $xmlParser,
  18. private PortfolioManager $portfolioManager,
  19. private LoggerInterface $logger,
  20. private TelegramNotifier $telegramNotifier,
  21. )
  22. {
  23. }
  24. public function fetchNewEmails(): void
  25. {
  26. $mailbox = $this->connection->getMailbox();
  27. try {
  28. // Get all emails (messages)
  29. // PHP.net imap_search criteria: http://php.net/manual/en/function.imap-search.php
  30. $mailsIds = $mailbox->searchMailbox('ALL');
  31. } catch (PhpImap\Exceptions\ConnectionException $ex) {
  32. $this->logger->error("IMAP connection failed: " . implode(",", $ex->getErrors('all')));
  33. die();
  34. }
  35. if (!$mailsIds) {
  36. $this->logger->info('Mailbox is empty');
  37. return;
  38. }
  39. // If '__DIR__' was defined in the first line, it will automatically
  40. // save all attachments to the specified directory
  41. foreach ($mailsIds as $mailId) {
  42. $mail = $mailbox->getMail($mailId);
  43. $this->logger->debug(print_r([
  44. 'date' => $mail->date,
  45. 'message_id' => $mail->messageId,
  46. 'from' => $mail->fromAddress,
  47. 'subject' => $mail->subject,
  48. 'hasAttachments' => $mail->hasAttachments(),
  49. ], true));
  50. if ($this->canProcessMail($mail)) {
  51. if ($this->processMail($mail)) {
  52. // for dev purposes
  53. //break;
  54. }
  55. $mailbox->deleteMail($mailId);
  56. $this->logger->debug("Deleted email with id=" . $mailId);
  57. } else {
  58. $this->logger->debug("Skipped mail with id=" . $mailId);
  59. }
  60. }
  61. $mailbox->disconnect();
  62. }
  63. private function canProcessMail(IncomingMail $mail): bool
  64. {
  65. if (!$mail->hasAttachments()) {
  66. return false;
  67. }
  68. if (mb_stripos($mail->subject, 'Broker report') === false) {
  69. return false;
  70. }
  71. if (mb_stripos($mail->fromAddress, '@bcs.ru') === false) {
  72. return false;
  73. }
  74. return true;
  75. }
  76. private function processMail(IncomingMail $mail): bool
  77. {
  78. $mailProcessed = false;
  79. foreach ($mail->getAttachments() as $attachment) {
  80. if ($attachment->mimeType != 'application/zip') {
  81. continue;
  82. }
  83. $mailProcessed |= $this->processZipArchiveAttachment($attachment);
  84. }
  85. return $mailProcessed;
  86. }
  87. private function processZipArchiveAttachment(IncomingMailAttachment $attachment): bool
  88. {
  89. $zip = new \ZipArchive();
  90. if ($zip->open($attachment->filePath) !== true) {
  91. $this->logger->error('ZIP open error: ' . $zip);
  92. return false;
  93. }
  94. $xmlFound = false;
  95. for ($i = 0; $i < $zip->numFiles; $i++) {
  96. $stat = $zip->statIndex($i);
  97. if (pathinfo($stat['name'])['extension'] !== 'xml') {
  98. continue;
  99. }
  100. $tmpdir = $this->getTmpDir();
  101. $zip->extractTo($tmpdir, [$stat['name']]);
  102. $this->logger->debug("Extracted '$stat[name]' into '$tmpdir'");
  103. $xmlString = file_get_contents($tmpdir . '/' . $stat['name']);
  104. $xml = simplexml_load_string($xmlString);
  105. $parsedPortfolio = $this->xmlParser->processXml($xml);
  106. $message = (new PresentationPortfolio)->toPrint($parsedPortfolio);
  107. if ($this->portfolioManager->updatePortfolio($parsedPortfolio, $xmlString)) {
  108. $this->telegramNotifier->notify('<pre>' . $message . '</pre>');
  109. }
  110. $fs = new Filesystem();
  111. $fs->remove($tmpdir);
  112. $xmlFound = true;
  113. }
  114. return $xmlFound;
  115. }
  116. private function getTmpDir(): string
  117. {
  118. $tmpdir = Path::normalize(sys_get_temp_dir() . '/FINFOLLOW-' . random_int(0, 100));
  119. $filesystem = new Filesystem();
  120. $filesystem->mkdir($tmpdir);
  121. return $tmpdir;
  122. }
  123. }