Passed
Pull Request — staging (#6107)
by Jakub
27:32 queued 12:02
created

EmailModel::addCompanyFilter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 3
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://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 Doctrine\DBAL\Query\QueryBuilder;
15
use Mautic\ChannelBundle\Entity\MessageQueue;
16
use Mautic\ChannelBundle\Model\MessageQueueModel;
17
use Mautic\CoreBundle\Helper\Chart\BarChart;
18
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
19
use Mautic\CoreBundle\Helper\Chart\LineChart;
20
use Mautic\CoreBundle\Helper\Chart\PieChart;
21
use Mautic\CoreBundle\Helper\DateTimeHelper;
22
use Mautic\CoreBundle\Helper\IpLookupHelper;
23
use Mautic\CoreBundle\Helper\ThemeHelper;
24
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
25
use Mautic\CoreBundle\Model\BuilderModelTrait;
26
use Mautic\CoreBundle\Model\FormModel;
27
use Mautic\CoreBundle\Model\TranslationModelTrait;
28
use Mautic\CoreBundle\Model\VariantModelTrait;
29
use Mautic\EmailBundle\EmailEvents;
30
use Mautic\EmailBundle\Entity\Email;
31
use Mautic\EmailBundle\Entity\Stat;
32
use Mautic\EmailBundle\Entity\StatDevice;
33
use Mautic\EmailBundle\Event\EmailBuilderEvent;
34
use Mautic\EmailBundle\Event\EmailEvent;
35
use Mautic\EmailBundle\Event\EmailOpenEvent;
36
use Mautic\EmailBundle\Event\EmailSendEvent;
37
use Mautic\EmailBundle\Exception\FailedToSendToContactException;
38
use Mautic\EmailBundle\Helper\MailHelper;
39
use Mautic\EmailBundle\MonitoredEmail\Mailbox;
40
use Mautic\LeadBundle\Entity\DoNotContact;
41
use Mautic\LeadBundle\Entity\Lead;
42
use Mautic\LeadBundle\Model\CompanyModel;
43
use Mautic\LeadBundle\Model\LeadModel;
44
use Mautic\LeadBundle\Tracker\DeviceTracker;
45
use Mautic\PageBundle\Model\TrackableModel;
46
use Mautic\UserBundle\Model\UserModel;
47
use Symfony\Component\Console\Helper\ProgressBar;
48
use Symfony\Component\Console\Output\OutputInterface;
49
use Symfony\Component\EventDispatcher\Event;
50
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
51
52
/**
53
 * Class EmailModel
54
 * {@inheritdoc}
55
 */
56
class EmailModel extends FormModel implements AjaxLookupModelInterface
57
{
58
    use VariantModelTrait;
59
    use TranslationModelTrait;
60
    use BuilderModelTrait;
61
62
    /**
63
     * @var IpLookupHelper
64
     */
65
    protected $ipLookupHelper;
66
67
    /**
68
     * @var ThemeHelper
69
     */
70
    protected $themeHelper;
71
72
    /**
73
     * @var Mailbox
74
     */
75
    protected $mailboxHelper;
76
77
    /**
78
     * @var MailHelper
79
     */
80
    protected $mailHelper;
81
82
    /**
83
     * @var LeadModel
84
     */
85
    protected $leadModel;
86
87
    /**
88
     * @var CompanyModel
89
     */
90
    protected $companyModel;
91
92
    /**
93
     * @var TrackableModel
94
     */
95
    protected $pageTrackableModel;
96
97
    /**
98
     * @var UserModel
99
     */
100
    protected $userModel;
101
102
    /**
103
     * @var MessageQueueModel
104
     */
105
    protected $messageQueueModel;
106
107
    /**
108
     * @var bool
109
     */
110
    protected $updatingTranslationChildren = false;
111
112
    /**
113
     * @var array
114
     */
115
    protected $emailSettings = [];
116
117
    /**
118
     * @var SendEmailToContact
119
     */
120
    protected $sendModel;
121
122
    /**
123
     * @var DeviceTracker
124
     */
125
    private $deviceTracker;
126
127
    /**
128
     * EmailModel constructor.
129
     *
130
     * @param IpLookupHelper     $ipLookupHelper
131
     * @param ThemeHelper        $themeHelper
132
     * @param Mailbox            $mailboxHelper
133
     * @param MailHelper         $mailHelper
134
     * @param LeadModel          $leadModel
135
     * @param CompanyModel       $companyModel
136
     * @param TrackableModel     $pageTrackableModel
137
     * @param UserModel          $userModel
138
     * @param MessageQueueModel  $messageQueueModel
139
     * @param SendEmailToContact $sendModel
140
     * @param DeviceTracker      $deviceTracker
141
     */
142
    public function __construct(
143
        IpLookupHelper $ipLookupHelper,
144
        ThemeHelper $themeHelper,
145
        Mailbox $mailboxHelper,
146
        MailHelper $mailHelper,
147
        LeadModel $leadModel,
148
        CompanyModel $companyModel,
149
        TrackableModel $pageTrackableModel,
150
        UserModel $userModel,
151
        MessageQueueModel $messageQueueModel,
152
        SendEmailToContact $sendModel,
153
        DeviceTracker $deviceTracker
154
    ) {
155
        $this->ipLookupHelper        = $ipLookupHelper;
156
        $this->themeHelper           = $themeHelper;
157
        $this->mailboxHelper         = $mailboxHelper;
158
        $this->mailHelper            = $mailHelper;
159
        $this->leadModel             = $leadModel;
160
        $this->companyModel          = $companyModel;
161
        $this->pageTrackableModel    = $pageTrackableModel;
162
        $this->userModel             = $userModel;
163
        $this->messageQueueModel     = $messageQueueModel;
164
        $this->sendModel             = $sendModel;
165
        $this->deviceTracker         = $deviceTracker;
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     *
171
     * @return \Mautic\EmailBundle\Entity\EmailRepository
172
     */
173
    public function getRepository()
174
    {
175
        return $this->em->getRepository('MauticEmailBundle:Email');
176
    }
177
178
    /**
179
     * @return \Mautic\EmailBundle\Entity\StatRepository
180
     */
181
    public function getStatRepository()
182
    {
183
        return $this->em->getRepository('MauticEmailBundle:Stat');
184
    }
185
186
    /**
187
     * @return \Mautic\EmailBundle\Entity\CopyRepository
188
     */
189
    public function getCopyRepository()
190
    {
191
        return $this->em->getRepository('MauticEmailBundle:Copy');
192
    }
193
194
    /**
195
     * @return \Mautic\EmailBundle\Entity\StatDeviceRepository
196
     */
197
    public function getStatDeviceRepository()
198
    {
199
        return $this->em->getRepository('MauticEmailBundle:StatDevice');
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function getPermissionBase()
206
    {
207
        return 'email:emails';
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     *
213
     * @param Email $entity
214
     * @param       $unlock
215
     *
216
     * @return mixed
217
     */
218
    public function saveEntity($entity, $unlock = true)
219
    {
220
        $type = $entity->getEmailType();
221
        if (empty($type)) {
222
            // Just in case JS failed
223
            $entity->setEmailType('template');
224
        }
225
226
        // Ensure that list emails are published
227
        if ($entity->getEmailType() == 'list') {
228
            // Ensure that this email has the same lists assigned as the translated parent if applicable
229
            /** @var Email $translationParent */
230
            if ($translationParent = $entity->getTranslationParent()) {
231
                $parentLists = $translationParent->getLists()->toArray();
232
                $entity->setLists($parentLists);
233
            }
234
        } else {
235
            // Ensure that all lists are been removed in case of a clone
236
            $entity->setLists([]);
237
        }
238
239
        if (!$this->updatingTranslationChildren) {
240
            if (!$entity->isNew()) {
241
                //increase the revision
242
                $revision = $entity->getRevision();
243
                ++$revision;
244
                $entity->setRevision($revision);
245
            }
246
247
            // Reset a/b test if applicable
248
            if ($isVariant = $entity->isVariant()) {
249
                $variantStartDate = new \DateTime();
250
                $resetVariants    = $this->preVariantSaveEntity($entity, ['setVariantSentCount', 'setVariantReadCount'], $variantStartDate);
251
            }
252
253
            parent::saveEntity($entity, $unlock);
254
255
            if ($isVariant) {
256
                $emailIds = $entity->getRelatedEntityIds();
257
                $this->postVariantSaveEntity($entity, $resetVariants, $emailIds, $variantStartDate);
258
            }
259
260
            $this->postTranslationEntitySave($entity);
261
262
            // Force translations for this entity to use the same segments
263
            if ($entity->getEmailType() == 'list' && $entity->hasTranslations()) {
264
                $translations                      = $entity->getTranslationChildren()->toArray();
265
                $this->updatingTranslationChildren = true;
266
                foreach ($translations as $translation) {
267
                    $this->saveEntity($translation);
268
                }
269
                $this->updatingTranslationChildren = false;
270
            }
271
        } else {
272
            parent::saveEntity($entity, false);
273
        }
274
    }
275
276
    /**
277
     * Save an array of entities.
278
     *
279
     * @param  $entities
280
     * @param  $unlock
281
     *
282
     * @return array
283
     */
284
    public function saveEntities($entities, $unlock = true)
285
    {
286
        //iterate over the results so the events are dispatched on each delete
287
        $batchSize = 20;
288
        foreach ($entities as $k => $entity) {
289
            $isNew = ($entity->getId()) ? false : true;
290
291
            //set some defaults
292
            $this->setTimestamps($entity, $isNew, $unlock);
293
294
            if ($dispatchEvent = $entity instanceof Email) {
295
                $event = $this->dispatchEvent('pre_save', $entity, $isNew);
296
            }
297
298
            $this->getRepository()->saveEntity($entity, false);
299
300
            if ($dispatchEvent) {
301
                $this->dispatchEvent('post_save', $entity, $isNew, $event);
302
            }
303
304
            if ((($k + 1) % $batchSize) === 0) {
305
                $this->em->flush();
306
            }
307
        }
308
        $this->em->flush();
309
    }
310
311
    /**
312
     * @param Email $entity
313
     */
314
    public function deleteEntity($entity)
315
    {
316
        if ($entity->isVariant() && $entity->getIsPublished()) {
317
            $this->resetVariants($entity);
318
        }
319
320
        parent::deleteEntity($entity);
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     *
326
     * @param       $entity
327
     * @param       $formFactory
328
     * @param null  $action
329
     * @param array $options
330
     *
331
     * @return mixed
332
     *
333
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
334
     */
335
    public function createForm($entity, $formFactory, $action = null, $options = [])
336
    {
337
        if (!$entity instanceof Email) {
338
            throw new MethodNotAllowedHttpException(['Email']);
339
        }
340
        if (!empty($action)) {
341
            $options['action'] = $action;
342
        }
343
344
        return $formFactory->create('emailform', $entity, $options);
345
    }
346
347
    /**
348
     * Get a specific entity or generate a new one if id is empty.
349
     *
350
     * @param $id
351
     *
352
     * @return null|Email
353
     */
354
    public function getEntity($id = null)
355
    {
356
        if ($id === null) {
357
            $entity = new Email();
358
            $entity->setSessionId('new_'.hash('sha1', uniqid(mt_rand())));
359
        } else {
360
            $entity = parent::getEntity($id);
361
            if ($entity !== null) {
362
                $entity->setSessionId($entity->getId());
363
            }
364
        }
365
366
        return $entity;
367
    }
368
369
    /**
370
     * {@inheritdoc}
371
     *
372
     * @param $action
373
     * @param $event
374
     * @param $entity
375
     * @param $isNew
376
     *
377
     * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
378
     */
379
    protected function dispatchEvent($action, &$entity, $isNew = false, Event $event = null)
380
    {
381
        if (!$entity instanceof Email) {
382
            throw new MethodNotAllowedHttpException(['Email']);
383
        }
384
385
        switch ($action) {
386
            case 'pre_save':
387
                $name = EmailEvents::EMAIL_PRE_SAVE;
388
                break;
389
            case 'post_save':
390
                $name = EmailEvents::EMAIL_POST_SAVE;
391
                break;
392
            case 'pre_delete':
393
                $name = EmailEvents::EMAIL_PRE_DELETE;
394
                break;
395
            case 'post_delete':
396
                $name = EmailEvents::EMAIL_POST_DELETE;
397
                break;
398
            default:
399
                return null;
400
        }
401
402
        if ($this->dispatcher->hasListeners($name)) {
403
            if (empty($event)) {
404
                $event = new EmailEvent($entity, $isNew);
405
                $event->setEntityManager($this->em);
406
            }
407
408
            $this->dispatcher->dispatch($name, $event);
409
410
            return $event;
411
        } else {
412
            return null;
413
        }
414
    }
415
416
    /**
417
     * @param      $stat
418
     * @param      $request
419
     * @param bool $viaBrowser
420
     *
421
     * @throws \Exception
422
     */
423
    public function hitEmail($stat, $request, $viaBrowser = false, $activeRequest = true)
424
    {
425
        if (!$stat instanceof Stat) {
426
            $stat = $this->getEmailStatus($stat);
427
        }
428
429
        if (!$stat) {
0 ignored issues
show
introduced by
$stat is of type Mautic\EmailBundle\Entity\Stat, thus it always evaluated to true.
Loading history...
430
            return;
431
        }
432
433
        $email = $stat->getEmail();
434
435
        if ((int) $stat->isRead()) {
436
            if ($viaBrowser && !$stat->getViewedInBrowser()) {
437
                //opened via browser so note it
438
                $stat->setViewedInBrowser($viaBrowser);
439
            }
440
        }
441
442
        $readDateTime = new DateTimeHelper();
443
        $stat->setLastOpened($readDateTime->getDateTime());
444
445
        $lead = $stat->getLead();
446
        if (null !== $lead) {
447
            // Set the lead as current lead
448
            if ($activeRequest) {
449
                $this->leadModel->setCurrentLead($lead);
450
            } else {
451
                $this->leadModel->setSystemCurrentLead($lead);
452
            }
453
        }
454
455
        $firstTime = false;
456
        if (!$stat->getIsRead()) {
457
            $firstTime = true;
458
            $stat->setIsRead(true);
459
            $stat->setDateRead($readDateTime->getDateTime());
460
461
            // Only up counts if associated with both an email and lead
462
            if ($email && $lead) {
0 ignored issues
show
introduced by
$lead is of type Mautic\LeadBundle\Entity\Lead, thus it always evaluated to true.
Loading history...
463
                try {
464
                    $this->getRepository()->upCount($email->getId(), 'read', 1, $email->isVariant());
465
                } catch (\Exception $exception) {
466
                    error_log($exception);
467
                }
468
            }
469
        }
470
471
        if ($viaBrowser) {
472
            $stat->setViewedInBrowser($viaBrowser);
473
        }
474
475
        $stat->addOpenDetails(
476
            [
477
                'datetime'  => $readDateTime->toUtcString(),
478
                'useragent' => $request->server->get('HTTP_USER_AGENT'),
479
                'inBrowser' => $viaBrowser,
480
            ]
481
        );
482
483
        //check for existing IP
484
        $ipAddress = $this->ipLookupHelper->getIpAddress();
485
        $stat->setIpAddress($ipAddress);
486
487
        if ($this->dispatcher->hasListeners(EmailEvents::EMAIL_ON_OPEN)) {
488
            $event = new EmailOpenEvent($stat, $request, $firstTime);
489
            $this->dispatcher->dispatch(EmailEvents::EMAIL_ON_OPEN, $event);
490
        }
491
492
        if ($email) {
0 ignored issues
show
introduced by
$email is of type Mautic\EmailBundle\Entity\Email, thus it always evaluated to true.
Loading history...
493
            $this->em->persist($email);
494
        }
495
496
        $emailOpenStat = new StatDevice();
497
        $emailOpenStat->setIpAddress($ipAddress);
498
        $trackedDevice = $this->deviceTracker->createDeviceFromUserAgent($lead, $request->server->get('HTTP_USER_AGENT'));
499
        $emailOpenStat->setDevice($trackedDevice);
500
        $emailOpenStat->setDateOpened($readDateTime->toUtcString());
501
        $emailOpenStat->setStat($stat);
502
503
        $this->em->persist($stat);
504
        $this->em->persist($emailOpenStat);
505
        try {
506
            $this->em->flush();
507
        } catch (\Exception $ex) {
508
            if (MAUTIC_ENV === 'dev') {
509
                throw $ex;
510
            } else {
511
                $this->logger->addError(
512
                    $ex->getMessage(),
513
                    ['exception' => $ex]
514
                );
515
            }
516
        }
517
    }
518
519
    /**
520
     * Get array of page builder tokens from bundles subscribed PageEvents::PAGE_ON_BUILD.
521
     *
522
     * @param null|Email   $email
523
     * @param array|string $requestedComponents all | tokens | abTestWinnerCriteria
524
     * @param null|string  $tokenFilter
525
     *
526
     * @return array
527
     */
528
    public function getBuilderComponents(Email $email = null, $requestedComponents = 'all', $tokenFilter = null, $withBC = true)
529
    {
530
        $event = new EmailBuilderEvent($this->translator, $email, $requestedComponents, $tokenFilter);
531
        $this->dispatcher->dispatch(EmailEvents::EMAIL_ON_BUILD, $event);
532
533
        return $this->getCommonBuilderComponents($requestedComponents, $event);
534
    }
535
536
    /**
537
     * @param $idHash
538
     *
539
     * @return Stat
540
     */
541
    public function getEmailStatus($idHash)
542
    {
543
        return $this->getStatRepository()->getEmailStatus($idHash);
544
    }
545
546
    /**
547
     * Search for an email stat by email and lead IDs.
548
     *
549
     * @param $emailId
550
     * @param $leadId
551
     *
552
     * @return array
553
     */
554
    public function getEmailStati($emailId, $leadId)
555
    {
556
        return $this->getStatRepository()->findBy(
557
            [
558
                'email' => (int) $emailId,
559
                'lead'  => (int) $leadId,
560
            ],
561
            ['dateSent' => 'DESC']
562
        );
563
    }
564
565
    /**
566
     * Get a stats for email by list.
567
     *
568
     * @param                $email
569
     * @param bool           $includeVariants
570
     * @param \DateTime|null $dateFrom
571
     * @param \DateTime|null $dateTo
572
     *
573
     * @return array
574
     */
575
    public function getEmailListStats($email, $includeVariants = false, \DateTime $dateFrom = null, \DateTime $dateTo = null)
576
    {
577
        if (!$email instanceof Email) {
578
            $email = $this->getEntity($email);
579
        }
580
581
        $emailIds = ($includeVariants && ($email->isVariant() || $email->isTranslation())) ? $email->getRelatedEntityIds() : [$email->getId()];
582
583
        $lists     = $email->getLists();
584
        $listCount = count($lists);
585
        $chart     = new BarChart(
586
            [
587
                $this->translator->trans('mautic.email.sent'),
588
                $this->translator->trans('mautic.email.read'),
589
                $this->translator->trans('mautic.email.failed'),
590
                $this->translator->trans('mautic.email.clicked'),
591
                $this->translator->trans('mautic.email.unsubscribed'),
592
                $this->translator->trans('mautic.email.bounced'),
593
            ]
594
        );
595
596
        if ($listCount) {
597
            /** @var \Mautic\EmailBundle\Entity\StatRepository $statRepo */
598
            $statRepo = $this->em->getRepository('MauticEmailBundle:Stat');
599
600
            /** @var \Mautic\LeadBundle\Entity\DoNotContactRepository $dncRepo */
601
            $dncRepo = $this->em->getRepository('MauticLeadBundle:DoNotContact');
602
603
            /** @var \Mautic\PageBundle\Entity\TrackableRepository $trackableRepo */
604
            $trackableRepo = $this->em->getRepository('MauticPageBundle:Trackable');
605
606
            $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
607
            $key   = ($listCount > 1) ? 1 : 0;
608
609
            $sentCounts         = $statRepo->getSentCount($emailIds, $lists->getKeys(), $query);
610
            $readCounts         = $statRepo->getReadCount($emailIds, $lists->getKeys(), $query);
611
            $failedCounts       = $statRepo->getFailedCount($emailIds, $lists->getKeys(), $query);
612
            $clickCounts        = $trackableRepo->getCount('email', $emailIds, $lists->getKeys(), $query);
613
            $unsubscribedCounts = $dncRepo->getCount('email', $emailIds, DoNotContact::UNSUBSCRIBED, $lists->getKeys(), $query);
614
            $bouncedCounts      = $dncRepo->getCount('email', $emailIds, DoNotContact::BOUNCED, $lists->getKeys(), $query);
615
616
            foreach ($lists as $l) {
617
                $sentCount         = isset($sentCounts[$l->getId()]) ? $sentCounts[$l->getId()] : 0;
618
                $readCount         = isset($readCounts[$l->getId()]) ? $readCounts[$l->getId()] : 0;
619
                $failedCount       = isset($failedCounts[$l->getId()]) ? $failedCounts[$l->getId()] : 0;
620
                $clickCount        = isset($clickCounts[$l->getId()]) ? $clickCounts[$l->getId()] : 0;
621
                $unsubscribedCount = isset($unsubscribedCounts[$l->getId()]) ? $unsubscribedCounts[$l->getId()] : 0;
622
                $bouncedCount      = isset($bouncedCounts[$l->getId()]) ? $bouncedCounts[$l->getId()] : 0;
623
624
                $chart->setDataset(
625
                    $l->getName(),
626
                    [
627
                        $sentCount,
628
                        $readCount,
629
                        $failedCount,
630
                        $clickCount,
631
                        $unsubscribedCount,
632
                        $bouncedCount,
633
                    ],
634
                    $key
635
                );
636
637
                ++$key;
638
            }
639
640
            $combined = [
641
                $statRepo->getSentCount($emailIds, $lists->getKeys(), $query, true),
642
                $statRepo->getReadCount($emailIds, $lists->getKeys(), $query, true),
643
                $statRepo->getFailedCount($emailIds, $lists->getKeys(), $query, true),
644
                $trackableRepo->getCount('email', $emailIds, $lists->getKeys(), $query, true),
645
                $dncRepo->getCount('email', $emailIds, DoNotContact::UNSUBSCRIBED, $lists->getKeys(), $query, true),
646
                $dncRepo->getCount('email', $emailIds, DoNotContact::BOUNCED, $lists->getKeys(), $query, true),
647
            ];
648
649
            if ($listCount > 1) {
650
                $chart->setDataset(
651
                    $this->translator->trans('mautic.email.lists.combined'),
652
                    $combined,
653
                    0
654
                );
655
            }
656
        }
657
658
        return $chart->render();
659
    }
660
661
    /**
662
     * Get a stats for email by list.
663
     *
664
     * @param Email|int $email
665
     * @param bool      $includeVariants
666
     *
667
     * @return array
668
     */
669
    public function getEmailDeviceStats($email, $includeVariants = false, $dateFrom = null, $dateTo = null)
670
    {
671
        if (!$email instanceof Email) {
672
            $email = $this->getEntity($email);
673
        }
674
675
        $emailIds      = ($includeVariants) ? $email->getRelatedEntityIds() : [$email->getId()];
676
        $templateEmail = 'template' === $email->getEmailType();
677
        $results       = $this->getStatDeviceRepository()->getDeviceStats($emailIds, $dateFrom, $dateTo);
678
679
        // Organize by list_id (if a segment email) and/or device
680
        $stats   = [];
681
        $devices = [];
682
        foreach ($results as $result) {
683
            if (empty($result['device'])) {
684
                $result['device'] = $this->translator->trans('mautic.core.unknown');
685
            } else {
686
                $result['device'] = mb_substr($result['device'], 0, 12);
687
            }
688
            $devices[$result['device']] = $result['device'];
689
690
            if ($templateEmail) {
691
                // List doesn't matter
692
                $stats[$result['device']] = $result['count'];
693
            } elseif (null !== $result['list_id']) {
694
                if (!isset($stats[$result['list_id']])) {
695
                    $stats[$result['list_id']] = [];
696
                }
697
698
                if (!isset($stats[$result['list_id']][$result['device']])) {
699
                    $stats[$result['list_id']][$result['device']] = (int) $result['count'];
700
                } else {
701
                    $stats[$result['list_id']][$result['device']] += (int) $result['count'];
702
                }
703
            }
704
        }
705
706
        $listCount = 0;
707
        if (!$templateEmail) {
708
            $lists     = $email->getLists();
709
            $listNames = [];
710
            foreach ($lists as $l) {
711
                $listNames[$l->getId()] = $l->getName();
712
            }
713
            $listCount = count($listNames);
714
        }
715
716
        natcasesort($devices);
717
        $chart = new BarChart(array_values($devices));
718
719
        if ($templateEmail) {
720
            // Populate the data
721
            $chart->setDataset(
722
                null,
723
                array_values($stats),
724
                0
725
            );
726
        } else {
727
            $combined = [];
728
            $key      = ($listCount > 1) ? 1 : 0;
729
            foreach ($listNames as $id => $name) {
730
                // Fill in missing devices
731
                $listStats = [];
732
                foreach ($devices as $device) {
733
                    $listStat    = (!isset($stats[$id][$device])) ? 0 : $stats[$id][$device];
734
                    $listStats[] = $listStat;
735
736
                    if (!isset($combined[$device])) {
737
                        $combined[$device] = 0;
738
                    }
739
740
                    $combined[$device] += $listStat;
741
                }
742
743
                // Populate the data
744
                $chart->setDataset(
745
                    $name,
746
                    $listStats,
747
                    $key
748
                );
749
750
                ++$key;
751
            }
752
753
            if ($listCount > 1) {
754
                $chart->setDataset(
755
                    $this->translator->trans('mautic.email.lists.combined'),
756
                    array_values($combined),
757
                    0
758
                );
759
            }
760
        }
761
762
        return $chart->render();
763
    }
764
765
    /**
766
     * @param           $email
767
     * @param bool      $includeVariants
768
     * @param           $unit
769
     * @param \DateTime $dateFrom
770
     * @param \DateTime $dateTo
771
     *
772
     * @return array
773
     */
774
    public function getEmailGeneralStats($email, $includeVariants, $unit, \DateTime $dateFrom, \DateTime $dateTo)
775
    {
776
        if (!$email instanceof Email) {
777
            $email = $this->getEntity($email);
778
        }
779
780
        $filter = [
781
            'email_id' => ($includeVariants) ? $email->getRelatedEntityIds() : [$email->getId()],
782
            'flag'     => 'all',
783
        ];
784
785
        return $this->getEmailsLineChartData($unit, $dateFrom, $dateTo, null, $filter);
786
    }
787
788
    /**
789
     * Get an array of tracked links.
790
     *
791
     * @param $emailId
792
     *
793
     * @return array
794
     */
795
    public function getEmailClickStats($emailId)
796
    {
797
        return $this->pageTrackableModel->getTrackableList('email', $emailId);
798
    }
799
800
    /**
801
     * Get the number of leads this email will be sent to.
802
     *
803
     * @param Email $email
804
     * @param mixed $listId          Leads for a specific lead list
805
     * @param bool  $countOnly       If true, return count otherwise array of leads
806
     * @param int   $limit           Max number of leads to retrieve
807
     * @param bool  $includeVariants If false, emails sent to a variant will not be included
808
     * @param int   $minContactId    Filter by min contact ID
809
     * @param int   $maxContactId    Filter by max contact ID
810
     * @param bool  $countWithMaxMin Add min_id and max_id info to the count result
811
     *
812
     * @return int|array
813
     */
814
    public function getPendingLeads(
815
        Email $email,
816
        $listId = null,
817
        $countOnly = false,
818
        $limit = null,
819
        $includeVariants = true,
820
        $minContactId = null,
821
        $maxContactId = null,
822
        $countWithMaxMin = false
823
    ) {
824
        $variantIds = ($includeVariants) ? $email->getRelatedEntityIds() : null;
825
        $total      = $this->getRepository()->getEmailPendingLeads(
826
            $email->getId(),
827
            $variantIds,
828
            $listId,
829
            $countOnly,
830
            $limit,
831
            $minContactId,
832
            $maxContactId,
833
            $countWithMaxMin
834
        );
835
836
        return $total;
837
    }
838
839
    /**
840
     * @param Email $email
841
     * @param bool  $includeVariants
842
     *
843
     * @return array|int
844
     */
845
    public function getQueuedCounts(Email $email, $includeVariants = true)
846
    {
847
        $ids = ($includeVariants) ? $email->getRelatedEntityIds() : null;
848
        if (!in_array($email->getId(), $ids)) {
849
            $ids[] = $email->getId();
850
        }
851
852
        return $this->messageQueueModel->getQueuedChannelCount('email', $ids);
853
    }
854
855
    /**
856
     * Send an email to lead lists.
857
     *
858
     * @param Email           $email
859
     * @param array           $lists
860
     * @param int             $limit
861
     * @param bool            $batch        True to process and batch all pending leads
862
     * @param OutputInterface $output
863
     * @param int             $minContactId
864
     * @param int             $maxContactId
865
     *
866
     * @return array array(int $sentCount, int $failedCount, array $failedRecipientsByList)
867
     */
868
    public function sendEmailToLists(
869
        Email $email,
870
        $lists = null,
871
        $limit = null,
872
        $batch = false,
873
        OutputInterface $output = null,
874
        $minContactId = null,
875
        $maxContactId = null
876
    ) {
877
        //get the leads
878
        if (empty($lists)) {
879
            $lists = $email->getLists();
880
        }
881
882
        // Safety check
883
        if ('list' !== $email->getEmailType()) {
884
            return [0, 0, []];
885
        }
886
887
        // Doesn't make sense to send unpublished emails. Probably a user error.
888
        // @todo throw an exception in Mautic 3 here.
889
        if (!$email->isPublished()) {
890
            return [0, 0, []];
891
        }
892
893
        $options = [
894
            'source'        => ['email', $email->getId()],
895
            'allowResends'  => false,
896
            'customHeaders' => [
897
                'Precedence' => 'Bulk',
898
            ],
899
        ];
900
901
        $failedRecipientsByList = [];
902
        $sentCount              = 0;
903
        $failedCount            = 0;
904
905
        $progress = false;
906
        if ($batch && $output) {
907
            $progressCounter = 0;
908
            $totalLeadCount  = $this->getPendingLeads($email, null, true, null, true, $minContactId, $maxContactId);
909
            if (!$totalLeadCount) {
910
                return [0, 0, []];
911
            }
912
913
            // Broadcast send through CLI
914
            $output->writeln("\n<info>".$email->getName().'</info>');
915
            $progress = new ProgressBar($output, $totalLeadCount);
916
        }
917
918
        foreach ($lists as $list) {
919
            if (!$batch && $limit !== null && $limit <= 0) {
920
                // Hit the max for this batch
921
                break;
922
            }
923
924
            $options['listId'] = $list->getId();
925
            $leads             = $this->getPendingLeads($email, $list->getId(), false, $limit, true, $minContactId, $maxContactId);
926
            $leadCount         = count($leads);
927
928
            while ($leadCount) {
929
                $sentCount += $leadCount;
930
931
                if (!$batch && $limit != null) {
932
                    // Only retrieve the difference between what has already been sent and the limit
933
                    $limit -= $leadCount;
934
                }
935
936
                $listErrors = $this->sendEmail($email, $leads, $options);
937
938
                if (!empty($listErrors)) {
939
                    $listFailedCount = count($listErrors);
940
941
                    $sentCount -= $listFailedCount;
942
                    $failedCount += $listFailedCount;
943
944
                    $failedRecipientsByList[$options['listId']] = $listErrors;
945
                }
946
947
                if ($batch) {
948
                    if ($progress) {
949
                        $progressCounter += $leadCount;
950
                        $progress->setProgress($progressCounter);
951
                    }
952
953
                    // Get the next batch of leads
954
                    $leads     = $this->getPendingLeads($email, $list->getId(), false, $limit, true, $minContactId, $maxContactId);
955
                    $leadCount = count($leads);
956
                } else {
957
                    $leadCount = 0;
958
                }
959
            }
960
        }
961
962
        if ($progress) {
963
            $progress->finish();
964
        }
965
966
        return [$sentCount, $failedCount, $failedRecipientsByList];
967
    }
968
969
    /**
970
     * Gets template, stats, weights, etc for an email in preparation to be sent.
971
     *
972
     * @param Email $email
973
     * @param bool  $includeVariants
974
     *
975
     * @return array
976
     */
977
    public function &getEmailSettings(Email $email, $includeVariants = true)
978
    {
979
        if (empty($this->emailSettings[$email->getId()])) {
980
            //used to house slots so they don't have to be fetched over and over for same template
981
            // BC for Mautic v1 templates
982
            $slots = [];
983
            if ($template = $email->getTemplate()) {
984
                $slots[$template] = $this->themeHelper->getTheme($template)->getSlots('email');
985
            }
986
987
            //store the settings of all the variants in order to properly disperse the emails
988
            //set the parent's settings
989
            $emailSettings = [
990
                $email->getId() => [
991
                    'template'     => $email->getTemplate(),
992
                    'slots'        => $slots,
993
                    'sentCount'    => $email->getSentCount(),
994
                    'variantCount' => $email->getVariantSentCount(),
995
                    'isVariant'    => null !== $email->getVariantStartDate(),
996
                    'entity'       => $email,
997
                    'translations' => $email->getTranslations(true),
998
                    'languages'    => ['default' => $email->getId()],
999
                ],
1000
            ];
1001
1002
            if ($emailSettings[$email->getId()]['translations']) {
1003
                // Add in the sent counts for translations of this email
1004
                /** @var Email $translation */
1005
                foreach ($emailSettings[$email->getId()]['translations'] as $translation) {
1006
                    if ($translation->isPublished()) {
1007
                        $emailSettings[$email->getId()]['sentCount'] += $translation->getSentCount();
1008
                        $emailSettings[$email->getId()]['variantCount'] += $translation->getVariantSentCount();
1009
1010
                        // Prevent empty key due to misconfiguration - pretty much ignored
1011
                        if (!$language = $translation->getLanguage()) {
1012
                            $language = 'unknown';
1013
                        }
1014
                        $core = $this->getTranslationLocaleCore($language);
1015
                        if (!isset($emailSettings[$email->getId()]['languages'][$core])) {
1016
                            $emailSettings[$email->getId()]['languages'][$core] = [];
1017
                        }
1018
                        $emailSettings[$email->getId()]['languages'][$core][$language] = $translation->getId();
1019
                    }
1020
                }
1021
            }
1022
1023
            if ($includeVariants && $email->isVariant()) {
1024
                //get a list of variants for A/B testing
1025
                $childrenVariant = $email->getVariantChildren();
1026
1027
                if (count($childrenVariant)) {
1028
                    $variantWeight = 0;
1029
                    $totalSent     = $emailSettings[$email->getId()]['variantCount'];
1030
1031
                    foreach ($childrenVariant as $id => $child) {
1032
                        if ($child->isPublished()) {
1033
                            $useSlots = [];
1034
                            if ($template = $child->getTemplate()) {
1035
                                if (isset($slots[$template])) {
1036
                                    $useSlots = $slots[$template];
1037
                                } else {
1038
                                    $slots[$template] = $this->themeHelper->getTheme($template)->getSlots('email');
1039
                                    $useSlots         = $slots[$template];
1040
                                }
1041
                            }
1042
                            $variantSettings                = $child->getVariantSettings();
1043
                            $emailSettings[$child->getId()] = [
1044
                                'template'     => $child->getTemplate(),
1045
                                'slots'        => $useSlots,
1046
                                'sentCount'    => $child->getSentCount(),
1047
                                'variantCount' => $child->getVariantSentCount(),
1048
                                'isVariant'    => null !== $email->getVariantStartDate(),
1049
                                'weight'       => ($variantSettings['weight'] / 100),
1050
                                'entity'       => $child,
1051
                                'translations' => $child->getTranslations(true),
1052
                                'languages'    => ['default' => $child->getId()],
1053
                            ];
1054
1055
                            $variantWeight += $variantSettings['weight'];
1056
1057
                            if ($emailSettings[$child->getId()]['translations']) {
1058
                                // Add in the sent counts for translations of this email
1059
                                /** @var Email $translation */
1060
                                foreach ($emailSettings[$child->getId()]['translations'] as $translation) {
1061
                                    if ($translation->isPublished()) {
1062
                                        $emailSettings[$child->getId()]['sentCount'] += $translation->getSentCount();
1063
                                        $emailSettings[$child->getId()]['variantCount'] += $translation->getVariantSentCount();
1064
1065
                                        // Prevent empty key due to misconfiguration - pretty much ignored
1066
                                        if (!$language = $translation->getLanguage()) {
1067
                                            $language = 'unknown';
1068
                                        }
1069
                                        $core = $this->getTranslationLocaleCore($language);
1070
                                        if (!isset($emailSettings[$child->getId()]['languages'][$core])) {
1071
                                            $emailSettings[$child->getId()]['languages'][$core] = [];
1072
                                        }
1073
                                        $emailSettings[$child->getId()]['languages'][$core][$language] = $translation->getId();
1074
                                    }
1075
                                }
1076
                            }
1077
1078
                            $totalSent += $emailSettings[$child->getId()]['variantCount'];
1079
                        }
1080
                    }
1081
1082
                    //set parent weight
1083
                    $emailSettings[$email->getId()]['weight'] = ((100 - $variantWeight) / 100);
1084
                } else {
1085
                    $emailSettings[$email->getId()]['weight'] = 1;
1086
                }
1087
            }
1088
1089
            $this->emailSettings[$email->getId()] = $emailSettings;
1090
        }
1091
1092
        if ($includeVariants && $email->isVariant()) {
1093
            //now find what percentage of current leads should receive the variants
1094
            if (!isset($totalSent)) {
1095
                $totalSent = 0;
1096
                foreach ($this->emailSettings[$email->getId()] as $eid => $details) {
1097
                    $totalSent += $details['variantCount'];
1098
                }
1099
            }
1100
1101
            foreach ($this->emailSettings[$email->getId()] as $eid => &$details) {
1102
                // Determine the deficit for email ordering
1103
                if ($totalSent) {
1104
                    $details['weight_deficit'] = $details['weight'] - ($details['variantCount'] / $totalSent);
1105
                    $details['send_weight']    = ($details['weight'] - ($details['variantCount'] / $totalSent)) + $details['weight'];
1106
                } else {
1107
                    $details['weight_deficit'] = $details['weight'];
1108
                    $details['send_weight']    = $details['weight'];
1109
                }
1110
            }
1111
1112
            // Reorder according to send_weight so that campaigns which currently send one at a time alternate
1113
            uasort($this->emailSettings[$email->getId()], function ($a, $b) {
1114
                if ($a['weight_deficit'] === $b['weight_deficit']) {
1115
                    if ($a['variantCount'] === $b['variantCount']) {
1116
                        return 0;
1117
                    }
1118
1119
                    // if weight is the same - sort by least number sent
1120
                    return ($a['variantCount'] < $b['variantCount']) ? -1 : 1;
1121
                }
1122
1123
                // sort by the one with the greatest deficit first
1124
                return ($a['weight_deficit'] > $b['weight_deficit']) ? -1 : 1;
1125
            });
1126
        }
1127
1128
        return $this->emailSettings[$email->getId()];
1129
    }
1130
1131
    /**
1132
     * Send an email to lead(s).
1133
     *
1134
     * @param   $email
1135
     * @param   $leads
1136
     * @param   $options = array()
0 ignored issues
show
Documentation Bug introduced by
The doc comment = at position 0 could not be parsed: Unknown type name '=' at position 0 in =.
Loading history...
1137
     *                   array source array('model', 'id')
1138
     *                   array emailSettings
1139
     *                   int   listId
1140
     *                   bool  allowResends     If false, exact emails (by id) already sent to the lead will not be resent
1141
     *                   bool  ignoreDNC        If true, emails listed in the do not contact table will still get the email
1142
     *                   array assetAttachments Array of optional Asset IDs to attach
1143
     *
1144
     * @return mixed
1145
     *
1146
     * @throws \Doctrine\ORM\ORMException
1147
     */
1148
    public function sendEmail(Email $email, $leads, $options = [])
1149
    {
1150
        $listId              = (isset($options['listId'])) ? $options['listId'] : null;
1151
        $ignoreDNC           = (isset($options['ignoreDNC'])) ? $options['ignoreDNC'] : false;
1152
        $tokens              = (isset($options['tokens'])) ? $options['tokens'] : [];
1153
        $assetAttachments    = (isset($options['assetAttachments'])) ? $options['assetAttachments'] : [];
1154
        $customHeaders       = (isset($options['customHeaders'])) ? $options['customHeaders'] : [];
1155
        $emailType           = (isset($options['email_type'])) ? $options['email_type'] : '';
1156
        $isMarketing         = (in_array($emailType, ['marketing']) || !empty($listId));
1157
        $emailAttempts       = (isset($options['email_attempts'])) ? $options['email_attempts'] : 3;
1158
        $emailPriority       = (isset($options['email_priority'])) ? $options['email_priority'] : MessageQueue::PRIORITY_NORMAL;
1159
        $messageQueue        = (isset($options['resend_message_queue'])) ? $options['resend_message_queue'] : null;
1160
        $returnErrorMessages = (isset($options['return_errors'])) ? $options['return_errors'] : false;
1161
        $channel             = (isset($options['channel'])) ? $options['channel'] : null;
1162
        $dncAsError          = (isset($options['dnc_as_error'])) ? $options['dnc_as_error'] : false;
1163
        $errors              = [];
1164
1165
        if (empty($channel)) {
1166
            $channel = (isset($options['source'])) ? $options['source'] : [];
1167
        }
1168
1169
        if (!$email->getId()) {
1170
            return false;
1171
        }
1172
1173
        // Ensure $sendTo is indexed by lead ID
1174
        $leadIds     = [];
1175
        $singleEmail = false;
1176
        if (isset($leads['id'])) {
1177
            $singleEmail           = $leads['id'];
1178
            $leadIds[$leads['id']] = $leads['id'];
1179
            $leads                 = [$leads['id'] => $leads];
1180
            $sendTo                = $leads;
1181
        } else {
1182
            $sendTo = [];
1183
            foreach ($leads as $lead) {
1184
                $sendTo[$lead['id']]  = $lead;
1185
                $leadIds[$lead['id']] = $lead['id'];
1186
            }
1187
        }
1188
1189
        /** @var \Mautic\EmailBundle\Entity\EmailRepository $emailRepo */
1190
        $emailRepo = $this->getRepository();
1191
1192
        //get email settings such as templates, weights, etc
1193
        $emailSettings = &$this->getEmailSettings($email);
1194
1195
        if (!$ignoreDNC) {
1196
            $dnc = $emailRepo->getDoNotEmailList($leadIds);
1197
1198
            if (!empty($dnc)) {
1199
                foreach ($dnc as $removeMeId => $removeMeEmail) {
1200
                    if ($dncAsError) {
1201
                        $errors[$removeMeId] = $this->translator->trans('mautic.email.dnc');
1202
                    }
1203
                    unset($sendTo[$removeMeId]);
1204
                    unset($leadIds[$removeMeId]);
1205
                }
1206
            }
1207
        }
1208
1209
        // Process frequency rules for email
1210
        if ($isMarketing && count($sendTo)) {
1211
            $campaignEventId = (is_array($channel) && !empty($channel) && 'campaign.event' === $channel[0] && !empty($channel[1])) ? $channel[1]
1212
                : null;
1213
            $this->messageQueueModel->processFrequencyRules(
1214
                $sendTo,
1215
                'email',
1216
                $email->getId(),
1217
                $campaignEventId,
1218
                $emailAttempts,
1219
                $emailPriority,
1220
                $messageQueue
1221
            );
1222
        }
1223
1224
        //get a count of leads
1225
        $count = count($sendTo);
1226
1227
        //no one to send to so bail or if marketing email from a campaign has been put in a queue
1228
        if (empty($count)) {
1229
            if ($returnErrorMessages) {
1230
                return $singleEmail && isset($errors[$singleEmail]) ? $errors[$singleEmail] : $errors;
1231
            }
1232
1233
            return $singleEmail ? true : $errors;
1234
        }
1235
1236
        // Hydrate contacts with company profile fields
1237
        $this->getContactCompanies($sendTo);
1238
1239
        foreach ($emailSettings as $eid => $details) {
1240
            if (isset($details['send_weight'])) {
1241
                $emailSettings[$eid]['limit'] = ceil($count * $details['send_weight']);
1242
            } else {
1243
                $emailSettings[$eid]['limit'] = $count;
1244
            }
1245
        }
1246
1247
        // Randomize the contacts for statistic purposes
1248
        shuffle($sendTo);
1249
1250
        // Organize the contacts according to the variant and translation they are to receive
1251
        $groupedContactsByEmail = [];
1252
        $offset                 = 0;
1253
        foreach ($emailSettings as $eid => $details) {
1254
            if (empty($details['limit'])) {
1255
                continue;
1256
            }
1257
            $groupedContactsByEmail[$eid] = [];
1258
            if ($details['limit']) {
1259
                // Take a chunk of contacts based on variant weights
1260
                if ($batchContacts = array_slice($sendTo, $offset, $details['limit'])) {
1261
                    $offset += $details['limit'];
1262
1263
                    // Group contacts by preferred locale
1264
                    foreach ($batchContacts as $key => $contact) {
1265
                        if (!empty($contact['preferred_locale'])) {
1266
                            $locale     = $contact['preferred_locale'];
1267
                            $localeCore = $this->getTranslationLocaleCore($locale);
1268
1269
                            if (isset($details['languages'][$localeCore])) {
1270
                                if (isset($details['languages'][$localeCore][$locale])) {
1271
                                    // Exact match
1272
                                    $translatedId                                  = $details['languages'][$localeCore][$locale];
1273
                                    $groupedContactsByEmail[$eid][$translatedId][] = $contact;
1274
                                } else {
1275
                                    // Grab the closest match
1276
                                    $bestMatch                                     = array_keys($details['languages'][$localeCore])[0];
1277
                                    $translatedId                                  = $details['languages'][$localeCore][$bestMatch];
1278
                                    $groupedContactsByEmail[$eid][$translatedId][] = $contact;
1279
                                }
1280
1281
                                unset($batchContacts[$key]);
1282
                            }
1283
                        }
1284
                    }
1285
1286
                    // If there are any contacts left over, assign them to the default
1287
                    if (count($batchContacts)) {
1288
                        $translatedId                                = $details['languages']['default'];
1289
                        $groupedContactsByEmail[$eid][$translatedId] = $batchContacts;
1290
                    }
1291
                }
1292
            }
1293
        }
1294
1295
        foreach ($groupedContactsByEmail as $parentId => $translatedEmails) {
1296
            $useSettings = $emailSettings[$parentId];
1297
            foreach ($translatedEmails as $translatedId => $contacts) {
1298
                $emailEntity = ($translatedId === $parentId) ? $useSettings['entity'] : $useSettings['translations'][$translatedId];
1299
1300
                $this->sendModel->setEmail($emailEntity, $channel, $customHeaders, $assetAttachments, $useSettings['slots'])
1301
                    ->setListId($listId);
1302
1303
                foreach ($contacts as $contact) {
1304
                    try {
1305
                        $this->sendModel->setContact($contact, $tokens)
1306
                            ->send();
1307
1308
                        // Update $emailSetting so campaign a/b tests are handled correctly
1309
                        ++$emailSettings[$parentId]['sentCount'];
1310
1311
                        if (!empty($emailSettings[$parentId]['isVariant'])) {
1312
                            ++$emailSettings[$parentId]['variantCount'];
1313
                        }
1314
                    } catch (FailedToSendToContactException $exception) {
1315
                        // move along to the next contact
1316
                    }
1317
                }
1318
            }
1319
        }
1320
1321
        // Flush the queue and store pending email stats
1322
        $this->sendModel->finalFlush();
1323
1324
        // Get the errors to return
1325
        $errorMessages  = array_merge($errors, $this->sendModel->getErrors());
1326
        $failedContacts = $this->sendModel->getFailedContacts();
1327
1328
        // Get sent counts to update email stats
1329
        $sentCounts = $this->sendModel->getSentCounts();
1330
1331
        // Reset the model for the next send
1332
        $this->sendModel->reset();
1333
1334
        // Update sent counts
1335
        foreach ($sentCounts as $emailId => $count) {
1336
            // Retry a few times in case of deadlock errors
1337
            $strikes = 3;
1338
            while ($strikes >= 0) {
1339
                try {
1340
                    $this->getRepository()->upCount($emailId, 'sent', $count, $emailSettings[$emailId]['isVariant']);
1341
                    break;
1342
                } catch (\Exception $exception) {
1343
                    error_log($exception);
1344
                }
1345
                --$strikes;
1346
            }
1347
        }
1348
1349
        unset($emailSettings, $options, $sendTo);
1350
1351
        $success = empty($failedContacts);
1352
        if (!$success && $returnErrorMessages) {
1353
            return $singleEmail ? $errorMessages[$singleEmail] : $errorMessages;
1354
        }
1355
1356
        return $singleEmail ? $success : $failedContacts;
1357
    }
1358
1359
    /**
1360
     * Send an email to lead(s).
1361
     *
1362
     * @param Email     $email
1363
     * @param array|int $users
1364
     * @param array     $lead
1365
     * @param array     $tokens
1366
     * @param array     $assetAttachments
1367
     * @param bool      $saveStat
1368
     * @param array     $to
1369
     * @param array     $cc
1370
     * @param array     $bcc
1371
     *
1372
     * @return mixed
1373
     *
1374
     * @throws \Doctrine\ORM\ORMException
1375
     */
1376
    public function sendEmailToUser(
1377
        Email $email,
1378
        $users,
1379
        array $lead = null,
1380
        array $tokens = [],
1381
        array $assetAttachments = [],
1382
        $saveStat = false,
1383
        array $to = [],
1384
        array $cc = [],
1385
        array $bcc = []
1386
    ) {
1387
        if (!$emailId = $email->getId()) {
1388
            return false;
1389
        }
1390
1391
        // In case only user ID was provided
1392
        if (!is_array($users)) {
1393
            $users = [['id' => $users]];
1394
        }
1395
1396
        // Get email settings
1397
        $emailSettings = &$this->getEmailSettings($email, false);
1398
1399
        // No one to send to so bail
1400
        if (empty($users) && empty($to)) {
1401
            return false;
1402
        }
1403
1404
        $mailer = $this->mailHelper->getMailer();
1405
        $mailer->setLead($lead, true);
1406
        $mailer->setTokens($tokens);
1407
        $mailer->setEmail($email, false, $emailSettings[$emailId]['slots'], $assetAttachments, (!$saveStat));
1408
        $mailer->setCc($cc);
1409
        $mailer->setBcc($bcc);
1410
1411
        $errors = [];
1412
1413
        foreach ($to as $toAddress) {
1414
            $idHash = uniqid();
1415
            $mailer->setIdHash($idHash, $saveStat);
1416
1417
            if (!$mailer->addTo($toAddress)) {
1418
                $errors[] = "{$toAddress}: ".$this->translator->trans('mautic.email.bounce.reason.bad_email');
1419
            } else {
1420
                if (!$mailer->queue(true)) {
1421
                    $errorArray = $mailer->getErrors();
1422
                    unset($errorArray['failures']);
1423
                    $errors[] = "{$toAddress}: ".implode('; ', $errorArray);
1424
                }
1425
1426
                if ($saveStat) {
1427
                    $saveEntities[] = $mailer->createEmailStat(false, $toAddress);
1428
                }
1429
1430
                // Clear CC and BCC to do not duplicate the send several times
1431
                $mailer->setCc([]);
1432
                $mailer->setBcc([]);
1433
            }
1434
        }
1435
1436
        foreach ($users as $user) {
1437
            $idHash = uniqid();
1438
            $mailer->setIdHash($idHash, $saveStat);
1439
1440
            if (!is_array($user)) {
1441
                $id   = $user;
1442
                $user = ['id' => $id];
1443
            } else {
1444
                $id = $user['id'];
1445
            }
1446
1447
            if (!isset($user['email'])) {
1448
                $userEntity        = $this->userModel->getEntity($id);
1449
                $user['email']     = $userEntity->getEmail();
1450
                $user['firstname'] = $userEntity->getFirstName();
1451
                $user['lastname']  = $userEntity->getLastName();
1452
            }
1453
1454
            if (!$mailer->setTo($user['email'], $user['firstname'].' '.$user['lastname'])) {
1455
                $errors[] = "{$user['email']}: ".$this->translator->trans('mautic.email.bounce.reason.bad_email');
1456
            } else {
1457
                if (!$mailer->queue(true)) {
1458
                    $errorArray = $mailer->getErrors();
1459
                    unset($errorArray['failures']);
1460
                    $errors[] = "{$user['email']}: ".implode('; ', $errorArray);
1461
                }
1462
1463
                if ($saveStat) {
1464
                    $saveEntities[] = $mailer->createEmailStat(false, $user['email']);
1465
                }
1466
1467
                // Clear CC and BCC to do not duplicate the send several times
1468
                $mailer->setCc([]);
1469
                $mailer->setBcc([]);
1470
            }
1471
        }
1472
1473
        //flush the message
1474
        if (!$mailer->flushQueue()) {
1475
            $errorArray = $mailer->getErrors();
1476
            unset($errorArray['failures']);
1477
            $errors[] = implode('; ', $errorArray);
1478
        }
1479
1480
        if (isset($saveEntities)) {
1481
            $this->getStatRepository()->saveEntities($saveEntities);
1482
        }
1483
1484
        //save some memory
1485
        unset($mailer);
1486
1487
        return $errors;
1488
    }
1489
1490
    /**
1491
     * Dispatches EmailSendEvent so you could get tokens form it or tokenized content.
1492
     *
1493
     * @param Email  $email
1494
     * @param array  $leadFields
1495
     * @param string $idHash
1496
     * @param array  $tokens
1497
     *
1498
     * @return EmailSendEvent
1499
     */
1500
    public function dispatchEmailSendEvent(Email $email, array $leadFields = [], $idHash = null, array $tokens = [])
1501
    {
1502
        $event = new EmailSendEvent(
1503
            null,
1504
            [
1505
                'content'      => $email->getCustomHtml(),
1506
                'email'        => $email,
1507
                'idHash'       => $idHash,
1508
                'tokens'       => $tokens,
1509
                'internalSend' => true,
1510
                'lead'         => $leadFields,
1511
            ]
1512
        );
1513
1514
        $this->dispatcher->dispatch(EmailEvents::EMAIL_ON_DISPLAY, $event);
1515
1516
        return $event;
1517
    }
1518
1519
    /**
1520
     * @param Stat $stat
1521
     * @param      $comments
1522
     * @param int  $reason
1523
     * @param bool $flush
1524
     *
1525
     * @return bool|DoNotContact
1526
     */
1527
    public function setDoNotContact(Stat $stat, $comments, $reason = DoNotContact::BOUNCED, $flush = true)
1528
    {
1529
        $lead = $stat->getLead();
1530
1531
        if ($lead instanceof Lead) {
0 ignored issues
show
introduced by
$lead is always a sub-type of Mautic\LeadBundle\Entity\Lead.
Loading history...
1532
            $email   = $stat->getEmail();
1533
            $channel = ($email) ? ['email' => $email->getId()] : 'email';
0 ignored issues
show
introduced by
$email is of type Mautic\EmailBundle\Entity\Email, thus it always evaluated to true.
Loading history...
1534
1535
            return $this->leadModel->addDncForLead($lead, $channel, $comments, $reason, $flush);
1536
        }
1537
1538
        return false;
1539
    }
1540
1541
    /**
1542
     * Remove a Lead's EMAIL DNC entry.
1543
     *
1544
     * @param string $email
1545
     */
1546
    public function removeDoNotContact($email)
1547
    {
1548
        /** @var \Mautic\LeadBundle\Entity\LeadRepository $leadRepo */
1549
        $leadRepo = $this->em->getRepository('MauticLeadBundle:Lead');
1550
        $leadId   = (array) $leadRepo->getLeadByEmail($email, true);
1551
1552
        /** @var \Mautic\LeadBundle\Entity\Lead[] $leads */
1553
        $leads = [];
1554
1555
        foreach ($leadId as $lead) {
1556
            $leads[] = $leadRepo->getEntity($lead['id']);
1557
        }
1558
1559
        foreach ($leads as $lead) {
1560
            $this->leadModel->removeDncForLead($lead, 'email');
1561
        }
1562
    }
1563
1564
    /**
1565
     * @param        $email
1566
     * @param int    $reason
1567
     * @param string $comments
1568
     * @param bool   $flush
1569
     * @param null   $leadId
1570
     *
1571
     * @return array
1572
     */
1573
    public function setEmailDoNotContact($email, $reason = DoNotContact::BOUNCED, $comments = '', $flush = true, $leadId = null)
1574
    {
1575
        /** @var \Mautic\LeadBundle\Entity\LeadRepository $leadRepo */
1576
        $leadRepo = $this->em->getRepository('MauticLeadBundle:Lead');
1577
1578
        if (null === $leadId) {
1579
            $leadId = (array) $leadRepo->getLeadByEmail($email, true);
1580
        } elseif (!is_array($leadId)) {
1581
            $leadId = [$leadId];
1582
        }
1583
1584
        $dnc = [];
1585
        foreach ($leadId as $lead) {
1586
            $dnc[] = $this->leadModel->addDncForLead(
1587
                $this->em->getReference('MauticLeadBundle:Lead', $lead),
1588
                'email',
1589
                $comments,
1590
                $reason,
1591
                $flush
1592
            );
1593
        }
1594
1595
        return $dnc;
1596
    }
1597
1598
    /**
1599
     * Processes the callback response from a mailer for bounces and unsubscribes.
1600
     *
1601
     * @deprecated 2.13.0 to be removed in 3.0; use TransportWebhook::processCallback() instead
1602
     *
1603
     * @param array $response
1604
     *
1605
     * @return array|void
1606
     */
1607
    public function processMailerCallback(array $response)
1608
    {
1609
        if (empty($response)) {
1610
            return;
1611
        }
1612
1613
        $statRepo = $this->getStatRepository();
1614
        $alias    = $statRepo->getTableAlias();
1615
        if (!empty($alias)) {
1616
            $alias .= '.';
1617
        }
1618
1619
        // Keep track to prevent duplicates before flushing
1620
        $emails = [];
1621
        $dnc    = [];
1622
1623
        foreach ($response as $type => $entries) {
1624
            if (!empty($entries['hashIds'])) {
1625
                $stats = $statRepo->getEntities(
1626
                    [
1627
                        'filter' => [
1628
                            'force' => [
1629
                                [
1630
                                    'column' => $alias.'trackingHash',
1631
                                    'expr'   => 'in',
1632
                                    'value'  => array_keys($entries['hashIds']),
1633
                                ],
1634
                            ],
1635
                        ],
1636
                    ]
1637
                );
1638
1639
                /** @var \Mautic\EmailBundle\Entity\Stat $s */
1640
                foreach ($stats as $s) {
1641
                    $reason = $entries['hashIds'][$s->getTrackingHash()];
1642
                    if ($this->translator->hasId('mautic.email.bounce.reason.'.$reason)) {
1643
                        $reason = $this->translator->trans('mautic.email.bounce.reason.'.$reason);
1644
                    }
1645
1646
                    $dnc[] = $this->setDoNotContact($s, $reason, $type);
1647
1648
                    $s->setIsFailed(true);
1649
                    $this->em->persist($s);
1650
                }
1651
            }
1652
1653
            if (!empty($entries['emails'])) {
1654
                foreach ($entries['emails'] as $email => $reason) {
1655
                    if (in_array($email, $emails)) {
1656
                        continue;
1657
                    }
1658
                    $emails[] = $email;
1659
1660
                    $leadId = null;
1661
                    if (is_array($reason)) {
1662
                        // Includes a lead ID
1663
                        $leadId = $reason['leadId'];
1664
                        $reason = $reason['reason'];
1665
                    }
1666
1667
                    if ($this->translator->hasId('mautic.email.bounce.reason.'.$reason)) {
1668
                        $reason = $this->translator->trans('mautic.email.bounce.reason.'.$reason);
1669
                    }
1670
1671
                    $dnc = array_merge($dnc, $this->setEmailDoNotContact($email, $type, $reason, true, $leadId));
1672
                }
1673
            }
1674
        }
1675
1676
        return $dnc;
1677
    }
1678
1679
    /**
1680
     * Get the settings for a monitored mailbox or false if not enabled.
1681
     *
1682
     * @param $bundleKey
1683
     * @param $folderKey
1684
     *
1685
     * @return bool|array
1686
     */
1687
    public function getMonitoredMailbox($bundleKey, $folderKey)
1688
    {
1689
        if ($this->mailboxHelper->isConfigured($bundleKey, $folderKey)) {
1690
            return $this->mailboxHelper->getMailboxSettings();
1691
        }
1692
1693
        return false;
1694
    }
1695
1696
    /**
1697
     * Joins the email table and limits created_by to currently logged in user.
1698
     *
1699
     * @param QueryBuilder $q
1700
     */
1701
    public function limitQueryToCreator(QueryBuilder &$q)
1702
    {
1703
        $q->join('t', MAUTIC_TABLE_PREFIX.'emails', 'e', 'e.id = t.email_id')
1704
            ->andWhere('e.created_by = :userId')
1705
            ->setParameter('userId', $this->userHelper->getUser()->getId());
1706
    }
1707
1708
    /**
1709
     * Get line chart data of emails sent and read.
1710
     *
1711
     * @param char      $unit          {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
1712
     * @param \DateTime $dateFrom
1713
     * @param \DateTime $dateTo
1714
     * @param string    $dateFormat
1715
     * @param array     $filter
1716
     * @param bool      $canViewOthers
1717
     *
1718
     * @return array
1719
     */
1720
    public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true)
1721
    {
1722
        $datasets   = [];
1723
        $flag       = null;
1724
        $companyId  = null;
1725
        $campaignId = null;
1726
        $segmentId  = null;
1727
1728
        if (isset($filter['flag'])) {
1729
            $flag = $filter['flag'];
1730
            unset($filter['flag']);
1731
        }
1732
        if (isset($filter['dataset'])) {
1733
            $datasets = array_merge($datasets, $filter['dataset']);
1734
            unset($filter['dataset']);
1735
        }
1736
        if (isset($filter['companyId'])) {
1737
            $companyId = $filter['companyId'];
1738
            unset($filter['companyId']);
1739
        }
1740
        if (isset($filter['campaignId'])) {
1741
            $campaignId = $filter['campaignId'];
1742
            unset($filter['campaignId']);
1743
        }
1744
        if (isset($filter['segmentId'])) {
1745
            $segmentId = $filter['segmentId'];
1746
            unset($filter['segmentId']);
1747
        }
1748
1749
        $chart = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
1750
        $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
1751
1752
        if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'sent_and_opened' || !$flag || in_array('sent', $datasets)) {
1753
            $q = $query->prepareTimeDataQuery('email_stats', 'date_sent', $filter);
1754
            if (!$canViewOthers) {
1755
                $this->limitQueryToCreator($q);
1756
            }
1757
            $this->addCompanyFilter($q, $companyId);
1758
            $this->addCampaignFilter($q, $campaignId);
1759
            $this->addSegmentFilter($q, $segmentId);
1760
            $data = $query->loadAndBuildTimeData($q);
1761
            $chart->setDataset($this->translator->trans('mautic.email.sent.emails'), $data);
1762
        }
1763
1764
        if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'sent_and_opened' || $flag == 'opened' || in_array('opened', $datasets)) {
1765
            $q = $query->prepareTimeDataQuery('email_stats', 'date_read', $filter);
1766
            if (!$canViewOthers) {
1767
                $this->limitQueryToCreator($q);
1768
            }
1769
            $this->addCompanyFilter($q, $companyId);
1770
            $this->addCampaignFilter($q, $campaignId);
1771
            $this->addSegmentFilter($q, $segmentId);
1772
            $data = $query->loadAndBuildTimeData($q);
1773
            $chart->setDataset($this->translator->trans('mautic.email.read.emails'), $data);
1774
        }
1775
1776
        if ($flag == 'sent_and_opened_and_failed' || $flag == 'all' || $flag == 'failed' || in_array('failed', $datasets)) {
1777
            $q = $query->prepareTimeDataQuery('email_stats', 'date_sent', $filter);
1778
            if (!$canViewOthers) {
1779
                $this->limitQueryToCreator($q);
1780
            }
1781
            $q->andWhere($q->expr()->eq('t.is_failed', ':true'))
1782
                ->setParameter('true', true, 'boolean');
1783
            $this->addCompanyFilter($q, $companyId);
1784
            $this->addCampaignFilter($q, $campaignId);
1785
            $this->addSegmentFilter($q, $segmentId);
1786
            $data = $query->loadAndBuildTimeData($q);
1787
            $chart->setDataset($this->translator->trans('mautic.email.failed.emails'), $data);
1788
        }
1789
1790
        if ($flag == 'all' || $flag == 'clicked' || in_array('clicked', $datasets)) {
1791
            $q = $query->prepareTimeDataQuery('page_hits', 'date_hit', []);
1792
            $q->leftJoin('t', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 't.source_id = es.email_id AND t.source = "email"');
1793
1794
            if (isset($filter['email_id'])) {
1795
                if (is_array($filter['email_id'])) {
1796
                    $q->andWhere('t.source_id IN (:email_ids)');
1797
                    $q->setParameter('email_ids', $filter['email_id'], \Doctrine\DBAL\Connection::PARAM_INT_ARRAY);
1798
                } else {
1799
                    $q->andWhere('t.source_id = :email_id');
1800
                    $q->setParameter('email_id', $filter['email_id']);
1801
                }
1802
            }
1803
1804
            if (!$canViewOthers) {
1805
                $this->limitQueryToCreator($q);
1806
            }
1807
            $this->addCompanyFilter($q, $companyId);
1808
            $this->addCampaignFilter($q, $campaignId);
1809
            $this->addSegmentFilter($q, $segmentId, 'es');
1810
            $data = $query->loadAndBuildTimeData($q);
1811
1812
            $chart->setDataset($this->translator->trans('mautic.email.clicked'), $data);
1813
        }
1814
1815
        if ($flag == 'all' || $flag == 'unsubscribed' || in_array('unsubscribed', $datasets)) {
1816
            $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::UNSUBSCRIBED, $canViewOthers, $companyId, $campaignId, $segmentId);
1817
            $chart->setDataset($this->translator->trans('mautic.email.unsubscribed'), $data);
1818
        }
1819
1820
        if ($flag == 'all' || $flag == 'bounced' || in_array('bounced', $datasets)) {
1821
            $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::BOUNCED, $canViewOthers, $companyId, $campaignId, $segmentId);
1822
            $chart->setDataset($this->translator->trans('mautic.email.bounced'), $data);
1823
        }
1824
1825
        return $chart->render();
1826
    }
1827
1828
    /**
1829
     * Modifies the line chart query for the DNC.
1830
     *
1831
     * @param ChartQuery $query
1832
     * @param array      $filter
1833
     * @param            $reason
1834
     * @param            $canViewOthers
1835
     * @param int|null   $companyId
1836
     * @param int|null   $campaignId
1837
     * @param int|null   $segmentId
1838
     *
1839
     * @return array
1840
     */
1841
    public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reason, $canViewOthers, $companyId = null, $campaignId = null, $segmentId = null)
1842
    {
1843
        $dncFilter = isset($filter['email_id']) ? ['channel_id' => $filter['email_id']] : [];
1844
        $q         = $query->prepareTimeDataQuery('lead_donotcontact', 'date_added', $dncFilter);
1845
        $q->andWhere('t.channel = :channel')
1846
            ->setParameter('channel', 'email')
1847
            ->andWhere($q->expr()->eq('t.reason', ':reason'))
1848
            ->setParameter('reason', $reason);
1849
1850
        $q->leftJoin('t', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 't.channel_id = es.email_id AND t.channel = "email" AND t.lead_id = es.lead_id');
1851
1852
        if (!$canViewOthers) {
1853
            $this->limitQueryToCreator($q);
1854
        }
1855
        $this->addCompanyFilter($q, $companyId);
1856
        $this->addCampaignFilter($q, $campaignId, 'es');
1857
        $this->addSegmentFilter($q, $segmentId, 'es');
1858
1859
        return $data = $query->loadAndBuildTimeData($q);
1860
    }
1861
1862
    /**
1863
     * @param QueryBuilder $q
1864
     * @param int|null     $companyId
1865
     * @param string       $fromAlias
1866
     */
1867
    private function addCompanyFilter(QueryBuilder $q, $companyId = null, $fromAlias = 't')
1868
    {
1869
        if ($companyId !== null) {
1870
            $q->innerJoin($fromAlias, MAUTIC_TABLE_PREFIX.'companies_leads', 'company_lead', $fromAlias.'.lead_id = company_lead.lead_id')
1871
                ->andWhere('company_lead.company_id = :companyId')
1872
                ->setParameter('companyId', $companyId);
1873
        }
1874
    }
1875
1876
    /**
1877
     * @param QueryBuilder $q
1878
     * @param int|null     $campaignId
1879
     * @param string       $fromAlias
1880
     */
1881
    private function addCampaignFilter(QueryBuilder $q, $campaignId = null, $fromAlias = 't')
1882
    {
1883
        if ($campaignId !== null) {
1884
            $q->innerJoin($fromAlias, MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'ce', $fromAlias.'.source_id = ce.event_id AND '.$fromAlias.'.source = "campaign.event" AND '.$fromAlias.'.lead_id = ce.lead_id')
1885
                ->innerJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id')
1886
                ->andWhere('ce.campaign_id = :campaignId')
1887
                ->setParameter('campaignId', $campaignId);
1888
        }
1889
    }
1890
1891
    /**
1892
     * @param QueryBuilder $q
1893
     * @param int|null     $segmentId
1894
     * @param string       $fromAlias
1895
     */
1896
    private function addSegmentFilter(QueryBuilder $q, $segmentId = null, $fromAlias = 't')
1897
    {
1898
        if ($segmentId !== null) {
1899
            $q->innerJoin($fromAlias, MAUTIC_TABLE_PREFIX.'lead_lists', 'll', $fromAlias.'.list_id = ll.id')
1900
                ->andWhere($fromAlias.'.list_id = :segmentId')
1901
                ->setParameter('segmentId', $segmentId);
1902
        }
1903
    }
1904
1905
    /**
1906
     * Get pie chart data of ignored vs opened emails.
1907
     *
1908
     * @param string $dateFrom
1909
     * @param string $dateTo
1910
     * @param array  $filters
1911
     * @param bool   $canViewOthers
1912
     *
1913
     * @return array
1914
     */
1915
    public function getIgnoredVsReadPieChartData($dateFrom, $dateTo, $filters = [], $canViewOthers = true)
1916
    {
1917
        $chart = new PieChart();
1918
        $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
1919
1920
        $readFilters                = $filters;
1921
        $readFilters['is_read']     = true;
1922
        $failedFilters              = $filters;
1923
        $failedFilters['is_failed'] = true;
1924
1925
        $sentQ   = $query->getCountQuery('email_stats', 'id', 'date_sent', $filters);
1926
        $readQ   = $query->getCountQuery('email_stats', 'id', 'date_sent', $readFilters);
1927
        $failedQ = $query->getCountQuery('email_stats', 'id', 'date_sent', $failedFilters);
1928
1929
        if (!$canViewOthers) {
1930
            $this->limitQueryToCreator($sentQ);
1931
            $this->limitQueryToCreator($readQ);
1932
            $this->limitQueryToCreator($failedQ);
1933
        }
1934
1935
        $sent   = $query->fetchCount($sentQ);
1936
        $read   = $query->fetchCount($readQ);
1937
        $failed = $query->fetchCount($failedQ);
1938
1939
        $chart->setDataset($this->translator->trans('mautic.email.graph.pie.ignored.read.failed.ignored'), ($sent - $read));
1940
        $chart->setDataset($this->translator->trans('mautic.email.graph.pie.ignored.read.failed.read'), $read);
1941
        $chart->setDataset($this->translator->trans('mautic.email.graph.pie.ignored.read.failed.failed'), $failed);
1942
1943
        return $chart->render();
1944
    }
1945
1946
    /**
1947
     * Get pie chart data of ignored vs opened emails.
1948
     *
1949
     * @param   $dateFrom
1950
     * @param   $dateTo
1951
     *
1952
     * @return array
1953
     */
1954
    public function getDeviceGranularityPieChartData($dateFrom, $dateTo)
1955
    {
1956
        $chart = new PieChart();
1957
1958
        $deviceStats = $this->getStatDeviceRepository()->getDeviceStats(
1959
            null,
1960
            $dateFrom,
1961
            $dateTo
1962
        );
1963
1964
        if (empty($deviceStats)) {
1965
            $deviceStats[] = [
1966
                'count'   => 0,
1967
                'device'  => $this->translator->trans('mautic.report.report.noresults'),
1968
                'list_id' => 0,
1969
            ];
1970
        }
1971
1972
        foreach ($deviceStats as $device) {
1973
            $chart->setDataset(
1974
                ($device['device']) ? $device['device'] : $this->translator->trans('mautic.core.unknown'),
1975
                $device['count']
1976
            );
1977
        }
1978
1979
        return $chart->render();
1980
    }
1981
1982
    /**
1983
     * Get a list of emails in a date range, grouped by a stat date count.
1984
     *
1985
     * @param int       $limit
1986
     * @param \DateTime $dateFrom
1987
     * @param \DateTime $dateTo
1988
     * @param array     $filters
1989
     * @param array     $options
1990
     *
1991
     * @return array
1992
     */
1993
    public function getEmailStatList($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $options = [])
1994
    {
1995
        $q = $this->em->getConnection()->createQueryBuilder();
1996
        $q->select('COUNT(DISTINCT t.id) AS count, e.id, e.name')
1997
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 't')
1998
            ->join('t', MAUTIC_TABLE_PREFIX.'emails', 'e', 'e.id = t.email_id')
1999
            ->orderBy('count', 'DESC')
2000
            ->groupBy('e.id')
2001
            ->setMaxResults($limit);
2002
2003
        if (!empty($options['canViewOthers'])) {
2004
            $q->andWhere('e.created_by = :userId')
2005
                ->setParameter('userId', $this->userHelper->getUser()->getId());
2006
        }
2007
2008
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2009
        $chartQuery->applyFilters($q, $filters);
2010
2011
        if (isset($options['groupBy']) && $options['groupBy'] == 'sends') {
2012
            $chartQuery->applyDateFilters($q, 'date_sent');
2013
        }
2014
2015
        if (isset($options['groupBy']) && $options['groupBy'] == 'reads') {
2016
            $chartQuery->applyDateFilters($q, 'date_read');
2017
        }
2018
2019
        $results = $q->execute()->fetchAll();
2020
2021
        return $results;
2022
    }
2023
2024
    /**
2025
     * Get a list of emails in a date range.
2026
     *
2027
     * @param int       $limit
2028
     * @param \DateTime $dateFrom
2029
     * @param \DateTime $dateTo
2030
     * @param array     $filters
2031
     * @param array     $options
2032
     *
2033
     * @return array
2034
     */
2035
    public function getEmailList($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $options = [])
2036
    {
2037
        $q = $this->em->getConnection()->createQueryBuilder();
2038
        $q->select('t.id, t.name, t.date_added, t.date_modified')
2039
            ->from(MAUTIC_TABLE_PREFIX.'emails', 't')
2040
            ->setMaxResults($limit);
2041
2042
        if (!empty($options['canViewOthers'])) {
2043
            $q->andWhere('t.created_by = :userId')
2044
                ->setParameter('userId', $this->userHelper->getUser()->getId());
2045
        }
2046
2047
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2048
        $chartQuery->applyFilters($q, $filters);
2049
        $chartQuery->applyDateFilters($q, 'date_added');
2050
2051
        $results = $q->execute()->fetchAll();
2052
2053
        return $results;
2054
    }
2055
2056
    /**
2057
     * Get a list of upcoming emails.
2058
     *
2059
     * @param int  $limit
2060
     * @param bool $canViewOthers
2061
     *
2062
     * @return array
2063
     */
2064
    public function getUpcomingEmails($limit = 10, $canViewOthers = true)
2065
    {
2066
        /** @var \Mautic\CampaignBundle\Entity\LeadEventLogRepository $leadEventLogRepository */
2067
        $leadEventLogRepository = $this->em->getRepository('MauticCampaignBundle:LeadEventLog');
2068
        $leadEventLogRepository->setCurrentUser($this->userHelper->getUser());
2069
        $upcomingEmails = $leadEventLogRepository->getUpcomingEvents(
2070
            [
2071
                'type'          => 'email.send',
2072
                'limit'         => $limit,
2073
                'canViewOthers' => $canViewOthers,
2074
            ]
2075
        );
2076
2077
        return $upcomingEmails;
2078
    }
2079
2080
    /**
2081
     * @deprecated 2.1 - use $entity->getVariants() instead; to be removed in 3.0
2082
     *
2083
     * @param Email $entity
2084
     *
2085
     * @return array
2086
     */
2087
    public function getVariants(Email $entity)
2088
    {
2089
        return $entity->getVariants();
2090
    }
2091
2092
    /**
2093
     * @param        $type
2094
     * @param string $filter
2095
     * @param int    $limit
2096
     * @param int    $start
2097
     * @param array  $options
2098
     *
2099
     * @return array
2100
     */
2101
    public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = [])
2102
    {
2103
        $results = [];
2104
        switch ($type) {
2105
            case 'email':
2106
                $emailRepo = $this->getRepository();
2107
                $emailRepo->setCurrentUser($this->userHelper->getUser());
2108
                $emails = $emailRepo->getEmailList(
2109
                    $filter,
2110
                    $limit,
2111
                    $start,
2112
                    $this->security->isGranted('email:emails:viewother'),
2113
                    isset($options['top_level']) ? $options['top_level'] : false,
2114
                    isset($options['email_type']) ? $options['email_type'] : null,
2115
                    isset($options['ignore_ids']) ? $options['ignore_ids'] : [],
2116
                    isset($options['variant_parent']) ? $options['variant_parent'] : null
2117
                );
2118
2119
                foreach ($emails as $email) {
2120
                    $results[$email['language']][$email['id']] = $email['name'];
2121
                }
2122
2123
                //sort by language
2124
                ksort($results);
2125
2126
                break;
2127
        }
2128
2129
        return $results;
2130
    }
2131
2132
    /**
2133
     * @param $sendTo
2134
     */
2135
    private function getContactCompanies(array &$sendTo)
2136
    {
2137
        $fetchCompanies = [];
2138
        foreach ($sendTo as $key => $contact) {
2139
            if (!isset($contact['companies'])) {
2140
                $fetchCompanies[$contact['id']] = $key;
2141
                $sendTo[$key]['companies']      = [];
2142
            }
2143
        }
2144
2145
        if (!empty($fetchCompanies)) {
2146
            // Simple dbal query that fetches lead_id IN $fetchCompanies and returns as array
2147
            $companies = $this->companyModel->getRepository()->getCompaniesForContacts(array_keys($fetchCompanies));
2148
2149
            foreach ($companies as $contactId => $contactCompanies) {
2150
                $key                       = $fetchCompanies[$contactId];
2151
                $sendTo[$key]['companies'] = $contactCompanies;
2152
            }
2153
        }
2154
    }
2155
2156
    /**
2157
     * Send an email to lead(s).
2158
     *
2159
     * @param       $email
2160
     * @param       $users
2161
     * @param mixed $leadFields
2162
     * @param array $tokens
2163
     * @param array $assetAttachments
2164
     * @param bool  $saveStat
2165
     *
2166
     * @return mixed
2167
     *
2168
     * @throws \Doctrine\ORM\ORMException
2169
     */
2170
    public function sendSampleEmailToUser($email, $users, $leadFields = null, $tokens = [], $assetAttachments = [], $saveStat = true)
2171
    {
2172
        if (!$emailId = $email->getId()) {
2173
            return false;
2174
        }
2175
2176
        if (!is_array($users)) {
2177
            $user  = ['id' => $users];
2178
            $users = [$user];
2179
        }
2180
2181
        //get email settings
2182
        $emailSettings = &$this->getEmailSettings($email, false);
2183
2184
        //noone to send to so bail
2185
        if (empty($users)) {
2186
            return false;
2187
        }
2188
2189
        $mailer = $this->mailHelper->getSampleMailer();
2190
        $mailer->setLead($leadFields, true);
2191
        $mailer->setTokens($tokens);
2192
        $mailer->setEmail($email, false, $emailSettings[$emailId]['slots'], $assetAttachments, (!$saveStat));
2193
2194
        $errors = [];
2195
        foreach ($users as $user) {
2196
            $idHash = uniqid();
2197
            $mailer->setIdHash($idHash, $saveStat);
2198
2199
            if (!is_array($user)) {
2200
                $id   = $user;
2201
                $user = ['id' => $id];
2202
            } else {
2203
                $id = $user['id'];
2204
            }
2205
2206
            if (!isset($user['email'])) {
2207
                $userEntity        = $this->userModel->getEntity($id);
2208
                $user['email']     = $userEntity->getEmail();
2209
                $user['firstname'] = $userEntity->getFirstName();
2210
                $user['lastname']  = $userEntity->getLastName();
2211
            }
2212
2213
            if (!$mailer->setTo($user['email'], $user['firstname'].' '.$user['lastname'])) {
2214
                $errors[] = "{$user['email']}: ".$this->translator->trans('mautic.email.bounce.reason.bad_email');
2215
            } else {
2216
                if (!$mailer->queue(true)) {
2217
                    $errorArray = $mailer->getErrors();
2218
                    unset($errorArray['failures']);
2219
                    $errors[] = "{$user['email']}: ".implode('; ', $errorArray);
2220
                }
2221
2222
                if ($saveStat) {
2223
                    $saveEntities[] = $mailer->createEmailStat(false, $user['email']);
2224
                }
2225
            }
2226
        }
2227
2228
        //flush the message
2229
        if (!$mailer->flushQueue()) {
2230
            $errorArray = $mailer->getErrors();
2231
            unset($errorArray['failures']);
2232
            $errors[] = implode('; ', $errorArray);
2233
        }
2234
2235
        if (isset($saveEntities)) {
2236
            $this->getStatRepository()->saveEntities($saveEntities);
2237
        }
2238
2239
        //save some memory
2240
        unset($mailer);
2241
2242
        return $errors;
2243
    }
2244
}
2245