Issues (3627)

bundles/EmailBundle/Model/SendEmailToContact.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2017 Mautic Contributors. All rights reserved
5
 * @author      Mautic, Inc.
6
 *
7
 * @link        https://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\EmailBundle\Model;
13
14
use Mautic\EmailBundle\Entity\Email;
15
use Mautic\EmailBundle\Entity\Stat;
16
use Mautic\EmailBundle\Entity\StatRepository;
17
use Mautic\EmailBundle\Exception\FailedToSendToContactException;
18
use Mautic\EmailBundle\Helper\MailHelper;
19
use Mautic\EmailBundle\Stat\Exception\StatNotFoundException;
20
use Mautic\EmailBundle\Stat\Reference;
21
use Mautic\EmailBundle\Stat\StatHelper;
22
use Mautic\EmailBundle\Swiftmailer\Exception\BatchQueueMaxException;
23
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
24
use Mautic\LeadBundle\Model\DoNotContact;
25
use Symfony\Component\Translation\TranslatorInterface;
26
27
class SendEmailToContact
28
{
29
    /**
30
     * @var MailHelper
31
     */
32
    private $mailer;
33
34
    /**
35
     * @var StatHelper
36
     */
37
    private $statHelper;
38
39
    /**
40
     * @var DoNotContact
41
     */
42
    private $dncModel;
43
44
    /**
45
     * @var TranslatorInterface
46
     */
47
    private $translator;
48
49
    /**
50
     * @var string|null
51
     */
52
    private $singleEmailMode;
53
54
    /**
55
     * @var array
56
     */
57
    private $failedContacts = [];
58
59
    /**
60
     * @var array
61
     */
62
    private $errorMessages = [];
63
64
    /**
65
     * @var array
66
     */
67
    private $badEmails = [];
68
69
    /**
70
     * @var array
71
     */
72
    private $emailSentCounts = [];
73
74
    /**
75
     * @var array|null
76
     */
77
    private $emailEntityErrors;
78
79
    /**
80
     * @var int|null
81
     */
82
    private $emailEntityId;
83
84
    /**
85
     * @var int|null
86
     */
87
    private $listId;
88
89
    /**
90
     * @var int
91
     */
92
    private $statBatchCounter = 0;
93
94
    /**
95
     * @var array
96
     */
97
    private $contact = [];
98
99
    /**
100
     * SendEmailToContact constructor.
101
     */
102
    public function __construct(MailHelper $mailer, StatHelper $statHelper, DoNotContact $dncModel, TranslatorInterface $translator)
103
    {
104
        $this->mailer     = $mailer;
105
        $this->statHelper = $statHelper;
106
        $this->dncModel   = $dncModel;
107
        $this->translator = $translator;
108
    }
109
110
    /**
111
     * @param bool $resetMailer
112
     *
113
     * @return $this
114
     */
115
    public function flush($resetMailer = true)
116
    {
117
        // Flushes the batch in case of using API mailers
118
        if ($this->emailEntityId && !$flushResult = $this->mailer->flushQueue()) {
119
            $sendFailures = $this->mailer->getErrors();
120
121
            // Check to see if failed recipients were stored by the transport
122
            if (!empty($sendFailures['failures'])) {
123
                $this->processSendFailures($sendFailures);
124
            } elseif ($this->singleEmailMode) {
125
                $this->errorMessages[$this->singleEmailMode] = implode('; ', $sendFailures);
126
            }
127
        }
128
129
        if ($resetMailer) {
130
            $this->mailer->reset(true);
131
        }
132
133
        return $this;
134
    }
135
136
    /**
137
     * Flush any remaining queued contacts, process spending stats, create DNC entries and reset this class.
138
     */
139
    public function finalFlush()
140
    {
141
        $this->flush();
142
        $this->statHelper->deletePending();
143
        $this->statHelper->reset();
144
145
        $this->processBadEmails();
146
    }
147
148
    /**
149
     * Use an Email entity to populate content, from, etc.
150
     *
151
     * @param array $channel ['channelName', 'channelId']
152
     *
153
     * @return $this
154
     */
155
    public function setEmail(Email $email, array $channel = [], array $customHeaders = [], array $assetAttachments = [])
156
    {
157
        // Flush anything that's pending from a previous email
158
        $this->flush();
159
160
        // Enable the queue if applicable to the transport
161
        $this->mailer->enableQueue();
162
163
        if ($this->mailer->setEmail($email, true, [], $assetAttachments)) {
164
            $this->mailer->setSource($channel);
165
            $this->mailer->setCustomHeaders($customHeaders);
166
167
            // Note that the entity is set so that addContact does not generate errors
168
            $this->emailEntityId = $email->getId();
169
        } else {
170
            // Fail all the contacts in this batch
171
            $this->emailEntityErrors = $this->mailer->getErrors();
172
            $this->emailEntityId     = null;
173
        }
174
175
        return $this;
176
    }
177
178
    /**
179
     * @param int|null $id
180
     *
181
     * @return $this
182
     */
183
    public function setListId($id)
184
    {
185
        $this->listId = empty($id) ? null : (int) $id;
186
187
        return $this;
188
    }
189
190
    /**
191
     * @return $this
192
     *
193
     * @throws FailedToSendToContactException
194
     */
195
    public function setContact(array $contact, array $tokens = [])
196
    {
197
        $this->contact = $contact;
198
199
        if (!$this->emailEntityId) {
200
            // There was an error configuring the email so auto fail
201
            $this->failContact(false, $this->emailEntityErrors);
202
        }
203
204
        $this->mailer->setTokens($tokens);
205
        $this->mailer->setLead($contact);
206
        $this->mailer->setIdHash(); //auto generates
207
208
        try {
209
            if (!$this->mailer->addTo($contact['email'], $contact['firstname'].' '.$contact['lastname'])) {
210
                $this->failContact();
211
            }
212
        } catch (BatchQueueMaxException $e) {
213
            // Queue full so flush then try again
214
            $this->flush(false);
215
216
            if (!$this->mailer->addTo($contact['email'], $contact['firstname'].' '.$contact['lastname'])) {
217
                $this->failContact();
218
            }
219
        }
220
221
        return $this;
222
    }
223
224
    /**
225
     * @throws FailedToSendToContactException
226
     */
227
    public function send()
228
    {
229
        if ($this->mailer->inTokenizationMode()) {
230
            list($success, $errors) = $this->queueTokenizedEmail();
231
        } else {
232
            list($success, $errors) = $this->sendStandardEmail();
233
        }
234
235
        //queue or send the message
236
        if (!$success) {
237
            unset($errors['failures']);
238
            $this->failContact(false, implode('; ', (array) $errors));
239
        }
240
    }
241
242
    /**
243
     * Reset everything.
244
     */
245
    public function reset()
246
    {
247
        [];
248
        [];
249
        [];
250
        $this->badEmails         = [];
251
        $this->errorMessages     = [];
252
        $this->failedContacts    = [];
253
        $this->emailEntityErrors = null;
254
        $this->emailEntityId     = null;
255
        $this->emailSentCounts   = [];
256
        $this->singleEmailMode   = null;
257
        $this->listId            = null;
258
        $this->statBatchCounter  = 0;
259
        $this->contact           = [];
260
261
        $this->dncModel->clearEntities();
262
263
        $this->mailer->reset();
264
    }
265
266
    /**
267
     * @return array
268
     */
269
    public function getSentCounts()
270
    {
271
        return $this->emailSentCounts;
272
    }
273
274
    /**
275
     * @return array
276
     */
277
    public function getErrors()
278
    {
279
        return $this->errorMessages;
280
    }
281
282
    /**
283
     * @return array
284
     */
285
    public function getFailedContacts()
286
    {
287
        return $this->failedContacts;
288
    }
289
290
    /**
291
     * @param bool  $hasBadEmail
292
     * @param array $errorMessages
293
     *
294
     * @throws FailedToSendToContactException
295
     */
296
    protected function failContact($hasBadEmail = true, $errorMessages = null)
297
    {
298
        if (null === $errorMessages) {
299
            // Clear the errors so it doesn't stop the next send
300
            $errorMessages = implode('; ', (array) $this->mailer->getErrors());
301
        } elseif (is_array($errorMessages)) {
0 ignored issues
show
The condition is_array($errorMessages) is always true.
Loading history...
302
            $errorMessages = implode('; ', $errorMessages);
303
        }
304
305
        $this->errorMessages[$this->contact['id']]  = $errorMessages;
306
        $this->failedContacts[$this->contact['id']] = $this->contact['email'];
307
308
        try {
309
            $stat = $this->statHelper->getStat($this->contact['email']);
310
            $this->downEmailSentCount($stat->getEmailId());
311
            $this->statHelper->markForDeletion($stat);
312
        } catch (StatNotFoundException $exception) {
313
        }
314
315
        if ($hasBadEmail) {
316
            $this->badEmails[$this->contact['id']] = $this->contact['email'];
317
        }
318
319
        throw new FailedToSendToContactException($errorMessages);
320
    }
321
322
    /**
323
     * @param $sendFailures
324
     */
325
    protected function processSendFailures($sendFailures)
326
    {
327
        $failedEmailAddresses = $sendFailures['failures'];
328
        unset($sendFailures['failures']);
329
        $error = implode('; ', $sendFailures);
330
331
        // Delete the stat
332
        foreach ($failedEmailAddresses as $failedEmail) {
333
            try {
334
                /** @var Reference $stat */
335
                $stat = $this->statHelper->getStat($failedEmail);
336
            } catch (StatNotFoundException $exception) {
337
                continue;
338
            }
339
340
            // Add lead ID to list of failures
341
            $this->failedContacts[$stat->getLeadId()]  = $failedEmail;
342
            $this->errorMessages[$stat->getLeadId()]   = $error;
343
344
            $this->statHelper->markForDeletion($stat);
345
346
            // Down sent counts
347
            $this->downEmailSentCount($stat->getEmailId());
348
        }
349
    }
350
351
    /**
352
     * Add DNC entries for bad emails to get them out of the queue permanently.
353
     */
354
    protected function processBadEmails()
355
    {
356
        // Update bad emails as bounces
357
        if (count($this->badEmails)) {
358
            foreach ($this->badEmails as $contactId => $contactEmail) {
359
                $this->dncModel->addDncForContact(
360
                    $contactId,
361
                    ['email' => $this->emailEntityId],
362
                    DNC::BOUNCED,
363
                    $this->translator->trans('mautic.email.bounce.reason.bad_email'),
364
                    true,
365
                    false
366
                );
367
            }
368
        }
369
    }
370
371
    /**
372
     * @param $email
373
     */
374
    protected function createContactStatEntry($email)
375
    {
376
        ++$this->statBatchCounter;
377
378
        $stat = $this->mailer->createEmailStat(false, null, $this->listId);
379
380
        // Store it in the statEntities array so that the stat can be deleted if the transport fails the
381
        // send for whatever reason after flushing the queue
382
        $this->statHelper->storeStat($stat, $email);
383
384
        $this->upEmailSentCount($stat->getEmail()->getId());
385
    }
386
387
    /**
388
     * Up sent counter for the given email ID.
389
     */
390
    protected function upEmailSentCount($emailId)
391
    {
392
        // Up sent counts
393
        if (!isset($this->emailSentCounts[$emailId])) {
394
            $this->emailSentCounts[$emailId] = 0;
395
        }
396
397
        ++$this->emailSentCounts[$emailId];
398
    }
399
400
    /**
401
     * Down sent counter for the given email ID.
402
     */
403
    protected function downEmailSentCount($emailId)
404
    {
405
        --$this->emailSentCounts[$emailId];
406
    }
407
408
    /**
409
     * @return array
410
     */
411
    protected function queueTokenizedEmail()
412
    {
413
        list($queued, $queueErrors) = $this->mailer->queue(true, MailHelper::QUEUE_RETURN_ERRORS);
414
415
        if ($queued) {
416
            // Create stat first to ensure it is available for emails sent immediately
417
            $this->createContactStatEntry($this->contact['email']);
418
        }
419
420
        return [$queued, $queueErrors];
421
    }
422
423
    /**
424
     * @return array
425
     */
426
    protected function sendStandardEmail()
427
    {
428
        // Dispatch the event to generate the tokens
429
        $this->mailer->dispatchSendEvent();
430
431
        // Create the stat to ensure it is availble for emails sent
432
        $this->createContactStatEntry($this->contact['email']);
433
434
        // Now send but don't redispatch the event
435
        return $this->mailer->queue(false, MailHelper::QUEUE_RETURN_ERRORS);
436
    }
437
}
438