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); | |
} | |
} | |