Spaces:
No application file
No application file
| namespace Mautic\EmailBundle\Model; | |
| use Mautic\EmailBundle\Entity\Email; | |
| use Mautic\EmailBundle\Exception\FailedToSendToContactException; | |
| use Mautic\EmailBundle\Helper\MailHelper; | |
| use Mautic\EmailBundle\Mailer\Exception\BatchQueueMaxException; | |
| use Mautic\EmailBundle\Stat\Exception\StatNotFoundException; | |
| use Mautic\EmailBundle\Stat\Reference; | |
| use Mautic\EmailBundle\Stat\StatHelper; | |
| use Mautic\LeadBundle\Entity\DoNotContact as DNC; | |
| use Mautic\LeadBundle\Model\DoNotContact; | |
| use Symfony\Contracts\Translation\TranslatorInterface; | |
| class SendEmailToContact | |
| { | |
| /** | |
| * @var string|null | |
| */ | |
| private $singleEmailMode; | |
| private array $failedContacts = []; | |
| /** | |
| * @var array | |
| */ | |
| private $errorMessages = []; | |
| /** | |
| * @var array | |
| */ | |
| private $badEmails = []; | |
| /** | |
| * @var array | |
| */ | |
| private $emailSentCounts = []; | |
| /** | |
| * @var array|null | |
| */ | |
| private $emailEntityErrors; | |
| /** | |
| * @var int|null | |
| */ | |
| private $emailEntityId; | |
| private ?int $listId = null; | |
| private int $statBatchCounter = 0; | |
| /** | |
| * @var array | |
| */ | |
| private $contact = []; | |
| public function __construct( | |
| private MailHelper $mailer, | |
| private StatHelper $statHelper, | |
| private DoNotContact $dncModel, | |
| private TranslatorInterface $translator | |
| ) { | |
| } | |
| /** | |
| * @param bool $resetMailer | |
| * | |
| * @return $this | |
| */ | |
| public function flush($resetMailer = true) | |
| { | |
| // Flushes the batch in case of using API mailers | |
| if ($this->emailEntityId && !$flushResult = $this->mailer->flushQueue()) { | |
| $sendFailures = $this->mailer->getErrors(); | |
| // Check to see if failed recipients were stored by the transport | |
| if (!empty($sendFailures['failures'])) { | |
| $this->processSendFailures($sendFailures); | |
| } elseif ($this->singleEmailMode) { | |
| $this->errorMessages[$this->singleEmailMode] = implode('; ', $sendFailures); | |
| } | |
| } | |
| if ($resetMailer) { | |
| $this->mailer->reset(true); | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Flush any remaining queued contacts, process spending stats, create DNC entries and reset this class. | |
| */ | |
| public function finalFlush(): void | |
| { | |
| $this->flush(); | |
| $this->statHelper->deletePending(); | |
| $this->statHelper->reset(); | |
| $this->processBadEmails(); | |
| } | |
| /** | |
| * Use an Email entity to populate content, from, etc. | |
| * | |
| * @param array $channel ['channelName', 'channelId'] | |
| * | |
| * @return $this | |
| */ | |
| public function setEmail(Email $email, array $channel = [], array $customHeaders = [], array $assetAttachments = [], string $emailType = null) | |
| { | |
| // Flush anything that's pending from a previous email | |
| $this->flush(); | |
| // Enable the queue if applicable to the transport | |
| $this->mailer->enableQueue(); | |
| if ($this->mailer->setEmail($email, true, [], $assetAttachments)) { | |
| $this->mailer->setEmailType($emailType); | |
| $this->mailer->setSource($channel); | |
| $this->mailer->setCustomHeaders($customHeaders); | |
| // Note that the entity is set so that addContact does not generate errors | |
| $this->emailEntityId = $email->getId(); | |
| } else { | |
| // Fail all the contacts in this batch | |
| $this->emailEntityErrors = $this->mailer->getErrors(); | |
| $this->emailEntityId = null; | |
| } | |
| return $this; | |
| } | |
| /** | |
| * @param int|null $id | |
| * | |
| * @return $this | |
| */ | |
| public function setListId($id) | |
| { | |
| $this->listId = empty($id) ? null : (int) $id; | |
| return $this; | |
| } | |
| /** | |
| * @return $this | |
| * | |
| * @throws FailedToSendToContactException | |
| */ | |
| public function setContact(array $contact, array $tokens = []) | |
| { | |
| $this->contact = $contact; | |
| if (!$this->emailEntityId) { | |
| // There was an error configuring the email so auto fail | |
| $this->failContact(false, $this->emailEntityErrors); | |
| } | |
| $this->mailer->setTokens($tokens); | |
| $this->mailer->setLead($contact); | |
| $this->mailer->setIdHash(); // auto generates | |
| try { | |
| if (!$this->mailer->addTo($contact['email'], $contact['firstname'].' '.$contact['lastname'])) { | |
| $this->failContact(); | |
| } | |
| } catch (BatchQueueMaxException) { | |
| // Queue full so flush then try again | |
| $this->flush(false); | |
| if (!$this->mailer->addTo($contact['email'], $contact['firstname'].' '.$contact['lastname'])) { | |
| $this->failContact(); | |
| } | |
| } | |
| return $this; | |
| } | |
| /** | |
| * @throws FailedToSendToContactException | |
| */ | |
| public function send(): void | |
| { | |
| if ($this->mailer->inTokenizationMode()) { | |
| [$success, $errors] = $this->queueTokenizedEmail(); | |
| } else { | |
| [$success, $errors] = $this->sendStandardEmail(); | |
| } | |
| // queue or send the message | |
| if (!$success) { | |
| unset($errors['failures']); | |
| $this->failContact(false, implode('; ', (array) $errors)); | |
| } | |
| } | |
| /** | |
| * Reset everything. | |
| */ | |
| public function reset(): void | |
| { | |
| $this->badEmails = []; | |
| $this->errorMessages = []; | |
| $this->failedContacts = []; | |
| $this->emailEntityErrors = null; | |
| $this->emailEntityId = null; | |
| $this->emailSentCounts = []; | |
| $this->singleEmailMode = null; | |
| $this->listId = null; | |
| $this->statBatchCounter = 0; | |
| $this->contact = []; | |
| $this->mailer->reset(); | |
| } | |
| /** | |
| * @return array | |
| */ | |
| public function getSentCounts() | |
| { | |
| return $this->emailSentCounts; | |
| } | |
| /** | |
| * @return array | |
| */ | |
| public function getErrors() | |
| { | |
| return $this->errorMessages; | |
| } | |
| /** | |
| * @return array | |
| */ | |
| public function getFailedContacts() | |
| { | |
| return $this->failedContacts; | |
| } | |
| /** | |
| * @param bool $hasBadEmail | |
| * @param array $errorMessages | |
| * | |
| * @throws FailedToSendToContactException | |
| */ | |
| protected function failContact($hasBadEmail = true, $errorMessages = null) | |
| { | |
| if (null === $errorMessages) { | |
| // Clear the errors so it doesn't stop the next send | |
| $errorMessages = implode('; ', (array) $this->mailer->getErrors()); | |
| } elseif (is_array($errorMessages)) { | |
| $errorMessages = implode('; ', $errorMessages); | |
| } | |
| $this->errorMessages[$this->contact['id']] = $errorMessages; | |
| $this->failedContacts[$this->contact['id']] = $this->contact['email']; | |
| try { | |
| $stat = $this->statHelper->getStat($this->contact['email']); | |
| $this->downEmailSentCount($stat->getEmailId()); | |
| $this->statHelper->markForDeletion($stat); | |
| } catch (StatNotFoundException) { | |
| } | |
| if ($hasBadEmail) { | |
| $this->badEmails[$this->contact['id']] = $this->contact['email']; | |
| } | |
| throw new FailedToSendToContactException($errorMessages); | |
| } | |
| protected function processSendFailures($sendFailures) | |
| { | |
| $failedEmailAddresses = $sendFailures['failures']; | |
| unset($sendFailures['failures']); | |
| $error = implode('; ', $sendFailures); | |
| // Delete the stat | |
| foreach ($failedEmailAddresses as $failedEmail) { | |
| try { | |
| /** @var Reference $stat */ | |
| $stat = $this->statHelper->getStat($failedEmail); | |
| } catch (StatNotFoundException) { | |
| continue; | |
| } | |
| // Add lead ID to list of failures | |
| $this->failedContacts[$stat->getLeadId()] = $failedEmail; | |
| $this->errorMessages[$stat->getLeadId()] = $error; | |
| $this->statHelper->markForDeletion($stat); | |
| // Down sent counts | |
| $this->downEmailSentCount($stat->getEmailId()); | |
| } | |
| } | |
| /** | |
| * Add DNC entries for bad emails to get them out of the queue permanently. | |
| */ | |
| protected function processBadEmails() | |
| { | |
| // Update bad emails as bounces | |
| if (count($this->badEmails)) { | |
| foreach ($this->badEmails as $contactId => $contactEmail) { | |
| $this->dncModel->addDncForContact( | |
| $contactId, | |
| ['email' => $this->emailEntityId], | |
| DNC::BOUNCED, | |
| $this->translator->trans('mautic.email.bounce.reason.bad_email'), | |
| true, | |
| false | |
| ); | |
| } | |
| } | |
| } | |
| protected function createContactStatEntry($email) | |
| { | |
| ++$this->statBatchCounter; | |
| $stat = $this->mailer->createEmailStat(false, null, $this->listId); | |
| // Store it in the statEntities array so that the stat can be deleted if the transport fails the | |
| // send for whatever reason after flushing the queue | |
| $this->statHelper->storeStat($stat, $email); | |
| $this->upEmailSentCount($stat->getEmail()->getId()); | |
| } | |
| /** | |
| * Up sent counter for the given email ID. | |
| */ | |
| protected function upEmailSentCount($emailId) | |
| { | |
| // Up sent counts | |
| if (!isset($this->emailSentCounts[$emailId])) { | |
| $this->emailSentCounts[$emailId] = 0; | |
| } | |
| ++$this->emailSentCounts[$emailId]; | |
| } | |
| /** | |
| * Down sent counter for the given email ID. | |
| */ | |
| protected function downEmailSentCount($emailId) | |
| { | |
| --$this->emailSentCounts[$emailId]; | |
| } | |
| protected function queueTokenizedEmail(): array | |
| { | |
| [$queued, $queueErrors] = $this->mailer->queue(true, MailHelper::QUEUE_RETURN_ERRORS); | |
| if ($queued) { | |
| // Create stat first to ensure it is available for emails sent immediately | |
| $this->createContactStatEntry($this->contact['email']); | |
| } | |
| return [$queued, $queueErrors]; | |
| } | |
| /** | |
| * @return array | |
| */ | |
| protected function sendStandardEmail() | |
| { | |
| // Dispatch the event to generate the tokens | |
| $this->mailer->dispatchSendEvent(); | |
| // Create the stat to ensure it is available for emails sent | |
| $this->createContactStatEntry($this->contact['email']); | |
| // Now send but don't redispatch the event | |
| return $this->mailer->queue(false, MailHelper::QUEUE_RETURN_ERRORS); | |
| } | |
| } | |