Issues (3627)

app/bundles/EmailBundle/Model/EmailModel.php (1 issue)

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\Doctrine\Provider\GeneratedColumnsProviderInterface;
18
use Mautic\CoreBundle\Helper\ArrayHelper;
19
use Mautic\CoreBundle\Helper\CacheStorageHelper;
20
use Mautic\CoreBundle\Helper\Chart\BarChart;
21
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
22
use Mautic\CoreBundle\Helper\Chart\LineChart;
23
use Mautic\CoreBundle\Helper\Chart\PieChart;
24
use Mautic\CoreBundle\Helper\DateTimeHelper;
25
use Mautic\CoreBundle\Helper\IpLookupHelper;
26
use Mautic\CoreBundle\Helper\ThemeHelper;
27
use Mautic\CoreBundle\Model\AjaxLookupModelInterface;
28
use Mautic\CoreBundle\Model\BuilderModelTrait;
29
use Mautic\CoreBundle\Model\FormModel;
30
use Mautic\CoreBundle\Model\TranslationModelTrait;
31
use Mautic\CoreBundle\Model\VariantModelTrait;
32
use Mautic\EmailBundle\EmailEvents;
33
use Mautic\EmailBundle\Entity\Email;
34
use Mautic\EmailBundle\Entity\Stat;
35
use Mautic\EmailBundle\Entity\StatDevice;
36
use Mautic\EmailBundle\Event\EmailBuilderEvent;
37
use Mautic\EmailBundle\Event\EmailEvent;
38
use Mautic\EmailBundle\Event\EmailOpenEvent;
39
use Mautic\EmailBundle\Event\EmailSendEvent;
40
use Mautic\EmailBundle\Exception\EmailCouldNotBeSentException;
41
use Mautic\EmailBundle\Exception\FailedToSendToContactException;
42
use Mautic\EmailBundle\Form\Type\EmailType;
43
use Mautic\EmailBundle\Helper\MailHelper;
44
use Mautic\EmailBundle\MonitoredEmail\Mailbox;
45
use Mautic\LeadBundle\Entity\DoNotContact;
46
use Mautic\LeadBundle\Entity\Lead;
47
use Mautic\LeadBundle\Model\CompanyModel;
48
use Mautic\LeadBundle\Model\DoNotContact as DNC;
49
use Mautic\LeadBundle\Model\LeadModel;
50
use Mautic\LeadBundle\Tracker\ContactTracker;
51
use Mautic\LeadBundle\Tracker\DeviceTracker;
52
use Mautic\PageBundle\Entity\RedirectRepository;
53
use Mautic\PageBundle\Model\TrackableModel;
54
use Mautic\UserBundle\Model\UserModel;
55
use Symfony\Component\Console\Helper\ProgressBar;
56
use Symfony\Component\Console\Output\OutputInterface;
57
use Symfony\Component\EventDispatcher\Event;
58
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
59
60
class EmailModel extends FormModel implements AjaxLookupModelInterface
61
{
62
    use VariantModelTrait;
63
    use TranslationModelTrait;
64
    use BuilderModelTrait;
65
66
    /**
67
     * @var IpLookupHelper
68
     */
69
    protected $ipLookupHelper;
70
71
    /**
72
     * @var ThemeHelper
73
     */
74
    protected $themeHelper;
75
76
    /**
77
     * @var Mailbox
78
     */
79
    protected $mailboxHelper;
80
81
    /**
82
     * @var MailHelper
83
     */
84
    protected $mailHelper;
85
86
    /**
87
     * @var LeadModel
88
     */
89
    protected $leadModel;
90
91
    /**
92
     * @var CompanyModel
93
     */
94
    protected $companyModel;
95
96
    /**
97
     * @var TrackableModel
98
     */
99
    protected $pageTrackableModel;
100
101
    /**
102
     * @var UserModel
103
     */
104
    protected $userModel;
105
106
    /**
107
     * @var MessageQueueModel
108
     */
109
    protected $messageQueueModel;
110
111
    /**
112
     * @var bool
113
     */
114
    protected $updatingTranslationChildren = false;
115
116
    /**
117
     * @var array
118
     */
119
    protected $emailSettings = [];
120
121
    /**
122
     * @var SendEmailToContact
123
     */
124
    protected $sendModel;
125
126
    /**
127
     * @var DeviceTracker
128
     */
129
    private $deviceTracker;
130
131
    /**
132
     * @var RedirectRepository
133
     */
134
    private $redirectRepository;
135
136
    /**
137
     * @var CacheStorageHelper
138
     */
139
    private $cacheStorageHelper;
140
141
    /**
142
     * @var ContactTracker
143
     */
144
    private $contactTracker;
145
146
    /**
147
     * @var DNC
148
     */
149
    private $doNotContact;
150
151
    /**
152
     * @var GeneratedColumnsProviderInterface
153
     */
154
    private $generatedColumnsProvider;
155
156
    public function __construct(
157
        IpLookupHelper $ipLookupHelper,
158
        ThemeHelper $themeHelper,
159
        Mailbox $mailboxHelper,
160
        MailHelper $mailHelper,
161
        LeadModel $leadModel,
162
        CompanyModel $companyModel,
163
        TrackableModel $pageTrackableModel,
164
        UserModel $userModel,
165
        MessageQueueModel $messageQueueModel,
166
        SendEmailToContact $sendModel,
167
        DeviceTracker $deviceTracker,
168
        RedirectRepository $redirectRepository,
169
        CacheStorageHelper $cacheStorageHelper,
170
        ContactTracker $contactTracker,
171
        DNC $doNotContact,
172
        GeneratedColumnsProviderInterface $generatedColumnsProvider
173
    ) {
174
        $this->ipLookupHelper           = $ipLookupHelper;
175
        $this->themeHelper              = $themeHelper;
176
        $this->mailboxHelper            = $mailboxHelper;
177
        $this->mailHelper               = $mailHelper;
178
        $this->leadModel                = $leadModel;
179
        $this->companyModel             = $companyModel;
180
        $this->pageTrackableModel       = $pageTrackableModel;
181
        $this->userModel                = $userModel;
182
        $this->messageQueueModel        = $messageQueueModel;
183
        $this->sendModel                = $sendModel;
184
        $this->deviceTracker            = $deviceTracker;
185
        $this->redirectRepository       = $redirectRepository;
186
        $this->cacheStorageHelper       = $cacheStorageHelper;
187
        $this->contactTracker           = $contactTracker;
188
        $this->doNotContact             = $doNotContact;
189
        $this->generatedColumnsProvider = $generatedColumnsProvider;
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     *
195
     * @return \Mautic\EmailBundle\Entity\EmailRepository
196
     */
197
    public function getRepository()
198
    {
199
        return $this->em->getRepository('MauticEmailBundle:Email');
200
    }
201
202
    /**
203
     * @return \Mautic\EmailBundle\Entity\StatRepository
204
     */
205
    public function getStatRepository()
206
    {
207
        return $this->em->getRepository('MauticEmailBundle:Stat');
208
    }
209
210
    /**
211
     * @return \Mautic\EmailBundle\Entity\CopyRepository
212
     */
213
    public function getCopyRepository()
214
    {
215
        return $this->em->getRepository('MauticEmailBundle:Copy');
216
    }
217
218
    /**
219
     * @return \Mautic\EmailBundle\Entity\StatDeviceRepository
220
     */
221
    public function getStatDeviceRepository()
222
    {
223
        return $this->em->getRepository('MauticEmailBundle:StatDevice');
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229
    public function getPermissionBase()
230
    {
231
        return 'email:emails';
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     *
237
     * @param Email $entity
238
     * @param       $unlock
239
     *
240
     * @return mixed
241
     */
242
    public function saveEntity($entity, $unlock = true)
243
    {
244
        $type = $entity->getEmailType();
245
        if (empty($type)) {
246
            // Just in case JS failed
247
            $entity->setEmailType('template');
248
        }
249
250
        // Ensure that list emails are published
251
        if ('list' == $entity->getEmailType()) {
252
            // Ensure that this email has the same lists assigned as the translated parent if applicable
253
            /** @var Email $translationParent */
254
            if ($translationParent = $entity->getTranslationParent()) {
255
                $parentLists = $translationParent->getLists()->toArray();
256
                $entity->setLists($parentLists);
257
            }
258
        } else {
259
            // Ensure that all lists are been removed in case of a clone
260
            $entity->setLists([]);
261
        }
262
263
        if (!$this->updatingTranslationChildren) {
264
            if (!$entity->isNew()) {
265
                //increase the revision
266
                $revision = $entity->getRevision();
267
                ++$revision;
268
                $entity->setRevision($revision);
269
            }
270
271
            // Reset a/b test if applicable
272
            if ($isVariant = $entity->isVariant()) {
273
                $variantStartDate = new \DateTime();
274
                $resetVariants    = $this->preVariantSaveEntity($entity, ['setVariantSentCount', 'setVariantReadCount'], $variantStartDate);
275
            }
276
277
            parent::saveEntity($entity, $unlock);
278
279
            if ($isVariant) {
280
                $emailIds = $entity->getRelatedEntityIds();
281
                $this->postVariantSaveEntity($entity, $resetVariants, $emailIds, $variantStartDate);
282
            }
283
284
            $this->postTranslationEntitySave($entity);
285
286
            // Force translations for this entity to use the same segments
287
            if ('list' == $entity->getEmailType() && $entity->hasTranslations()) {
288
                $translations                      = $entity->getTranslationChildren()->toArray();
289
                $this->updatingTranslationChildren = true;
290
                foreach ($translations as $translation) {
291
                    $this->saveEntity($translation);
292
                }
293
                $this->updatingTranslationChildren = false;
294
            }
295
        } else {
296
            parent::saveEntity($entity, false);
297
        }
298
    }
299
300
    /**
301
     * Save an array of entities.
302
     *
303
     * @param  $entities
304
     * @param  $unlock
305
     *
306
     * @return array
307
     */
308
    public function saveEntities($entities, $unlock = true)
309
    {
310
        //iterate over the results so the events are dispatched on each delete
311
        $batchSize = 20;
312
        $i         = 0;
313
        foreach ($entities as $entity) {
314
            $isNew = ($entity->getId()) ? false : true;
315
316
            //set some defaults
317
            $this->setTimestamps($entity, $isNew, $unlock);
318
319
            if ($dispatchEvent = $entity instanceof Email) {
320
                $event = $this->dispatchEvent('pre_save', $entity, $isNew);
321
            }
322
323
            $this->getRepository()->saveEntity($entity, false);
324
325
            if ($dispatchEvent) {
326
                $this->dispatchEvent('post_save', $entity, $isNew, $event);
327
            }
328
329
            if (0 === ++$i % $batchSize) {
330
                $this->em->flush();
331
            }
332
        }
333
        $this->em->flush();
334
    }
335
336
    /**
337
     * @param Email $entity
338
     */
339
    public function deleteEntity($entity)
340
    {
341
        if ($entity->isVariant() && $entity->getIsPublished()) {
342
            $this->resetVariants($entity);
343
        }
344
345
        parent::deleteEntity($entity);
346
    }
347
348
    /**
349
     * {@inheritdoc}
350
     *
351
     * @param       $entity
352
     * @param       $formFactory
353
     * @param null  $action
354
     * @param array $options
355
     *
356
     * @return mixed
357
     *
358
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
359
     */
360
    public function createForm($entity, $formFactory, $action = null, $options = [])
361
    {
362
        if (!$entity instanceof Email) {
363
            throw new MethodNotAllowedHttpException(['Email']);
364
        }
365
        if (!empty($action)) {
366
            $options['action'] = $action;
367
        }
368
369
        return $formFactory->create(EmailType::class, $entity, $options);
370
    }
371
372
    /**
373
     * Get a specific entity or generate a new one if id is empty.
374
     *
375
     * @param $id
376
     *
377
     * @return Email|null
378
     */
379
    public function getEntity($id = null)
380
    {
381
        if (null === $id) {
382
            $entity = new Email();
383
            $entity->setSessionId('new_'.hash('sha1', uniqid(mt_rand())));
384
        } else {
385
            $entity = parent::getEntity($id);
386
            if (null !== $entity) {
387
                $entity->setSessionId($entity->getId());
388
            }
389
        }
390
391
        return $entity;
392
    }
393
394
    /**
395
     * Return a list of entities.
396
     *
397
     * @param array $args [start, limit, filter, orderBy, orderByDir]
398
     *
399
     * @return \Doctrine\ORM\Tools\Pagination\Paginator|array
400
     */
401
    public function getEntities(array $args = [])
402
    {
403
        $entities = parent::getEntities($args);
404
405
        foreach ($entities as $entity) {
406
            $queued  = $this->cacheStorageHelper->get(sprintf('%s|%s|%s', 'email', $entity->getId(), 'queued'));
407
            $pending = $this->cacheStorageHelper->get(sprintf('%s|%s|%s', 'email', $entity->getId(), 'pending'));
408
409
            if (false !== $queued) {
410
                $entity->setQueuedCount($queued);
411
            }
412
413
            if (false !== $pending) {
414
                $entity->setPendingCount($pending);
415
            }
416
        }
417
418
        return $entities;
419
    }
420
421
    /**
422
     * {@inheritdoc}
423
     *
424
     * @param $action
425
     * @param $event
426
     * @param $entity
427
     * @param $isNew
428
     *
429
     * @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
430
     */
431
    protected function dispatchEvent($action, &$entity, $isNew = false, Event $event = null)
432
    {
433
        if (!$entity instanceof Email) {
434
            throw new MethodNotAllowedHttpException(['Email']);
435
        }
436
437
        switch ($action) {
438
            case 'pre_save':
439
                $name = EmailEvents::EMAIL_PRE_SAVE;
440
                break;
441
            case 'post_save':
442
                $name = EmailEvents::EMAIL_POST_SAVE;
443
                break;
444
            case 'pre_delete':
445
                $name = EmailEvents::EMAIL_PRE_DELETE;
446
                break;
447
            case 'post_delete':
448
                $name = EmailEvents::EMAIL_POST_DELETE;
449
                break;
450
            default:
451
                return null;
452
        }
453
454
        if ($this->dispatcher->hasListeners($name)) {
455
            if (empty($event)) {
456
                $event = new EmailEvent($entity, $isNew);
457
                $event->setEntityManager($this->em);
458
            }
459
460
            $this->dispatcher->dispatch($name, $event);
461
462
            return $event;
463
        } else {
464
            return null;
465
        }
466
    }
467
468
    /**
469
     * @param      $stat
470
     * @param      $request
471
     * @param bool $viaBrowser
472
     *
473
     * @throws \Exception
474
     */
475
    public function hitEmail($stat, $request, $viaBrowser = false, $activeRequest = true)
476
    {
477
        if (!$stat instanceof Stat) {
478
            $stat = $this->getEmailStatus($stat);
479
        }
480
481
        if (!$stat) {
482
            return;
483
        }
484
485
        $email = $stat->getEmail();
486
487
        if ((int) $stat->isRead()) {
488
            if ($viaBrowser && !$stat->getViewedInBrowser()) {
489
                //opened via browser so note it
490
                $stat->setViewedInBrowser($viaBrowser);
491
            }
492
        }
493
494
        $readDateTime = new DateTimeHelper();
495
        $stat->setLastOpened($readDateTime->getDateTime());
496
497
        $lead = $stat->getLead();
498
        if (null !== $lead) {
499
            // Set the lead as current lead
500
            if ($activeRequest) {
501
                $this->contactTracker->setTrackedContact($lead);
502
            } else {
503
                $this->contactTracker->setSystemContact($lead);
504
            }
505
        }
506
507
        $firstTime = false;
508
        if (!$stat->getIsRead()) {
509
            $firstTime = true;
510
            $stat->setIsRead(true);
511
            $stat->setDateRead($readDateTime->getDateTime());
512
513
            // Only up counts if associated with both an email and lead
514
            if ($email && $lead) {
515
                try {
516
                    $this->getRepository()->upCount($email->getId(), 'read', 1, $email->isVariant());
517
                } catch (\Exception $exception) {
518
                    error_log($exception);
519
                }
520
            }
521
        }
522
523
        if ($viaBrowser) {
524
            $stat->setViewedInBrowser($viaBrowser);
525
        }
526
527
        $stat->addOpenDetails(
528
            [
529
                'datetime'  => $readDateTime->toUtcString(),
530
                'useragent' => $request->server->get('HTTP_USER_AGENT'),
531
                'inBrowser' => $viaBrowser,
532
            ]
533
        );
534
535
        //check for existing IP
536
        $ipAddress = $this->ipLookupHelper->getIpAddress();
537
        $stat->setIpAddress($ipAddress);
538
539
        if ($this->dispatcher->hasListeners(EmailEvents::EMAIL_ON_OPEN)) {
540
            $event = new EmailOpenEvent($stat, $request, $firstTime);
541
            $this->dispatcher->dispatch(EmailEvents::EMAIL_ON_OPEN, $event);
542
        }
543
544
        if ($email) {
545
            $this->em->persist($email);
546
        }
547
548
        $this->em->persist($stat);
549
550
        // Flush the email stat entity in different transactions than the device stat entity to avoid deadlocks.
551
        $this->flushAndCatch();
552
553
        if ($lead) {
554
            $trackedDevice = $this->deviceTracker->createDeviceFromUserAgent($lead, $request->server->get('HTTP_USER_AGENT'));
555
            $emailOpenStat = new StatDevice();
556
            $emailOpenStat->setIpAddress($ipAddress);
557
            $emailOpenStat->setDevice($trackedDevice);
0 ignored issues
show
It seems like $trackedDevice can also be of type null; however, parameter $device of Mautic\EmailBundle\Entity\StatDevice::setDevice() does only seem to accept Mautic\LeadBundle\Entity\LeadDevice, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

557
            $emailOpenStat->setDevice(/** @scrutinizer ignore-type */ $trackedDevice);
Loading history...
558
            $emailOpenStat->setDateOpened($readDateTime->toUtcString());
559
            $emailOpenStat->setStat($stat);
560
561
            $this->em->persist($emailOpenStat);
562
            $this->flushAndCatch();
563
        }
564
    }
565
566
    /**
567
     * Get array of page builder tokens from bundles subscribed PageEvents::PAGE_ON_BUILD.
568
     *
569
     * @param array|string $requestedComponents all | tokens | abTestWinnerCriteria
570
     * @param string|null  $tokenFilter
571
     *
572
     * @return array
573
     */
574
    public function getBuilderComponents(Email $email = null, $requestedComponents = 'all', $tokenFilter = null, $withBC = true)
575
    {
576
        $event = new EmailBuilderEvent($this->translator, $email, $requestedComponents, $tokenFilter);
577
        $this->dispatcher->dispatch(EmailEvents::EMAIL_ON_BUILD, $event);
578
579
        return $this->getCommonBuilderComponents($requestedComponents, $event);
580
    }
581
582
    /**
583
     * @param          $limit
584
     * @param array    $options
585
     * @param int|null $companyId
586
     * @param int|null $campaignId
587
     * @param int|null $segmentId
588
     *
589
     * @return array
590
     */
591
    public function getSentEmailToContactData($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null, $campaignId = null, $segmentId = null)
592
    {
593
        $createdByUserId = null;
594
        $canViewOthers   = empty($options['canViewOthers']) ? false : $options['canViewOthers'];
595
596
        if (!$canViewOthers) {
597
            $createdByUserId = $this->userHelper->getUser()->getId();
598
        }
599
600
        $stats = $this->getStatRepository()->getSentEmailToContactData($limit, $dateFrom, $dateTo, $createdByUserId, $companyId, $campaignId, $segmentId);
601
        $data  = [];
602
603
        foreach ($stats as $stat) {
604
            $statId = $stat['id'];
605
606
            if (empty($stat['segment_id']) && !empty($stat['campaign_id'])) {
607
                // Let's fetch the segment based on current campaign/segment membership
608
                $segmentMembership = $this->em->getRepository('MauticCampaignBundle:Campaign')
609
                    ->getContactSingleSegmentByCampaign($stat['lead_id'], $stat['campaign_id']);
610
611
                if ($segmentMembership) {
612
                    $stat['segment_id']   = $segmentMembership['id'];
613
                    $stat['segment_name'] = $segmentMembership['name'];
614
                }
615
            }
616
617
            $item = [
618
                'contact_id'    => $stat['lead_id'],
619
                'contact_email' => $stat['email_address'],
620
                'open'          => $stat['is_read'],
621
                'click'         => (null !== $stat['link_hits']) ? $stat['link_hits'] : 0,
622
                'links_clicked' => [],
623
                'email_id'      => (string) $stat['email_id'],
624
                'email_name'    => (string) $stat['email_name'],
625
                'segment_id'    => (string) $stat['segment_id'],
626
                'segment_name'  => (string) $stat['segment_name'],
627
                'company_id'    => (string) $stat['company_id'],
628
                'company_name'  => (string) $stat['company_name'],
629
                'campaign_id'   => (string) $stat['campaign_id'],
630
                'campaign_name' => (string) $stat['campaign_name'],
631
                'date_sent'     => $stat['date_sent'],
632
                'date_read'     => $stat['date_read'],
633
            ];
634
635
            if ($item['click'] && $item['email_id'] && $item['contact_id']) {
636
                $item['links_clicked'] = $this->getStatRepository()->getUniqueClickedLinksPerContactAndEmail($item['contact_id'], $item['email_id']);
637
            }
638
639
            $data[$statId] = $item;
640
        }
641
642
        return $data;
643
    }
644
645
    /**
646
     * @param int      $limit
647
     * @param array    $options
648
     * @param int|null $companyId
649
     * @param int|null $campaignId
650
     * @param int|null $segmentId
651
     *
652
     * @return array
653
     */
654
    public function getMostHitEmailRedirects($limit, \DateTime $dateFrom, \DateTime $dateTo, $options = [], $companyId = null, $campaignId = null, $segmentId = null)
655
    {
656
        $createdByUserId = null;
657
        $canViewOthers   = empty($options['canViewOthers']) ? false : $options['canViewOthers'];
658
659
        if (!$canViewOthers) {
660
            $createdByUserId = $this->userHelper->getUser()->getId();
661
        }
662
663
        $redirects = $this->redirectRepository->getMostHitEmailRedirects($limit, $dateFrom, $dateTo, $createdByUserId, $companyId, $campaignId, $segmentId);
664
        $data      = [];
665
        foreach ($redirects as $redirect) {
666
            $data[] = [
667
                'url'         => (string) $redirect['url'],
668
                'unique_hits' => (string) $redirect['unique_hits'],
669
                'hits'        => (string) $redirect['hits'],
670
                'email_id'    => (string) $redirect['email_id'],
671
                'email_name'  => (string) $redirect['email_name'],
672
            ];
673
        }
674
675
        return $data;
676
    }
677
678
    /**
679
     * @param $idHash
680
     *
681
     * @return Stat
682
     */
683
    public function getEmailStatus($idHash)
684
    {
685
        return $this->getStatRepository()->getEmailStatus($idHash);
686
    }
687
688
    /**
689
     * Search for an email stat by email and lead IDs.
690
     *
691
     * @param $emailId
692
     * @param $leadId
693
     *
694
     * @return array
695
     */
696
    public function getEmailStati($emailId, $leadId)
697
    {
698
        return $this->getStatRepository()->findBy(
699
            [
700
                'email' => (int) $emailId,
701
                'lead'  => (int) $leadId,
702
            ],
703
            ['dateSent' => 'DESC']
704
        );
705
    }
706
707
    /**
708
     * Get a stats for email by list.
709
     *
710
     * @param      $email
711
     * @param bool $includeVariants
712
     *
713
     * @return array
714
     */
715
    public function getEmailListStats($email, $includeVariants = false, \DateTime $dateFrom = null, \DateTime $dateTo = null)
716
    {
717
        if (!$email instanceof Email) {
718
            $email = $this->getEntity($email);
719
        }
720
721
        $emailIds = ($includeVariants && ($email->isVariant() || $email->isTranslation())) ? $email->getRelatedEntityIds() : [$email->getId()];
722
723
        $lists     = $email->getLists();
724
        $listCount = count($lists);
725
        $chart     = new BarChart(
726
            [
727
                $this->translator->trans('mautic.email.sent'),
728
                $this->translator->trans('mautic.email.read'),
729
                $this->translator->trans('mautic.email.failed'),
730
                $this->translator->trans('mautic.email.clicked'),
731
                $this->translator->trans('mautic.email.unsubscribed'),
732
                $this->translator->trans('mautic.email.bounced'),
733
            ]
734
        );
735
736
        if ($listCount) {
737
            /** @var \Mautic\EmailBundle\Entity\StatRepository $statRepo */
738
            $statRepo = $this->em->getRepository('MauticEmailBundle:Stat');
739
740
            /** @var \Mautic\LeadBundle\Entity\DoNotContactRepository $dncRepo */
741
            $dncRepo = $this->em->getRepository('MauticLeadBundle:DoNotContact');
742
743
            /** @var \Mautic\PageBundle\Entity\TrackableRepository $trackableRepo */
744
            $trackableRepo = $this->em->getRepository('MauticPageBundle:Trackable');
745
746
            $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
747
            $key   = ($listCount > 1) ? 1 : 0;
748
749
            $sentCounts         = $statRepo->getSentCount($emailIds, $lists->getKeys(), $query);
750
            $readCounts         = $statRepo->getReadCount($emailIds, $lists->getKeys(), $query);
751
            $failedCounts       = $statRepo->getFailedCount($emailIds, $lists->getKeys(), $query);
752
            $clickCounts        = $trackableRepo->getCount('email', $emailIds, $lists->getKeys(), $query, false, 'DISTINCT ph.lead_id');
753
            $unsubscribedCounts = $dncRepo->getCount('email', $emailIds, DoNotContact::UNSUBSCRIBED, $lists->getKeys(), $query);
754
            $bouncedCounts      = $dncRepo->getCount('email', $emailIds, DoNotContact::BOUNCED, $lists->getKeys(), $query);
755
756
            foreach ($lists as $l) {
757
                $sentCount         = isset($sentCounts[$l->getId()]) ? $sentCounts[$l->getId()] : 0;
758
                $readCount         = isset($readCounts[$l->getId()]) ? $readCounts[$l->getId()] : 0;
759
                $failedCount       = isset($failedCounts[$l->getId()]) ? $failedCounts[$l->getId()] : 0;
760
                $clickCount        = isset($clickCounts[$l->getId()]) ? $clickCounts[$l->getId()] : 0;
761
                $unsubscribedCount = isset($unsubscribedCounts[$l->getId()]) ? $unsubscribedCounts[$l->getId()] : 0;
762
                $bouncedCount      = isset($bouncedCounts[$l->getId()]) ? $bouncedCounts[$l->getId()] : 0;
763
764
                $chart->setDataset(
765
                    $l->getName(),
766
                    [
767
                        $sentCount,
768
                        $readCount,
769
                        $failedCount,
770
                        $clickCount,
771
                        $unsubscribedCount,
772
                        $bouncedCount,
773
                    ],
774
                    $key
775
                );
776
777
                ++$key;
778
            }
779
780
            $combined = [
781
                $statRepo->getSentCount($emailIds, $lists->getKeys(), $query, true),
782
                $statRepo->getReadCount($emailIds, $lists->getKeys(), $query, true),
783
                $statRepo->getFailedCount($emailIds, $lists->getKeys(), $query, true),
784
                $trackableRepo->getCount('email', $emailIds, $lists->getKeys(), $query, true, 'DISTINCT ph.lead_id'),
785
                $dncRepo->getCount('email', $emailIds, DoNotContact::UNSUBSCRIBED, $lists->getKeys(), $query, true),
786
                $dncRepo->getCount('email', $emailIds, DoNotContact::BOUNCED, $lists->getKeys(), $query, true),
787
            ];
788
789
            if ($listCount > 1) {
790
                $chart->setDataset(
791
                    $this->translator->trans('mautic.email.lists.combined'),
792
                    $combined,
793
                    0
794
                );
795
            }
796
        }
797
798
        return $chart->render();
799
    }
800
801
    /**
802
     * Get a stats for email by list.
803
     *
804
     * @param Email|int $email
805
     * @param bool      $includeVariants
806
     *
807
     * @return array
808
     */
809
    public function getEmailDeviceStats($email, $includeVariants = false, $dateFrom = null, $dateTo = null)
810
    {
811
        if (!$email instanceof Email) {
812
            $email = $this->getEntity($email);
813
        }
814
815
        $emailIds      = ($includeVariants) ? $email->getRelatedEntityIds() : [$email->getId()];
816
        $templateEmail = 'template' === $email->getEmailType();
817
        $results       = $this->getStatDeviceRepository()->getDeviceStats($emailIds, $dateFrom, $dateTo);
818
819
        // Organize by list_id (if a segment email) and/or device
820
        $stats   = [];
821
        $devices = [];
822
        foreach ($results as $result) {
823
            if (empty($result['device'])) {
824
                $result['device'] = $this->translator->trans('mautic.core.unknown');
825
            } else {
826
                $result['device'] = mb_substr($result['device'], 0, 12);
827
            }
828
            $devices[$result['device']] = $result['device'];
829
830
            if ($templateEmail) {
831
                // List doesn't matter
832
                $stats[$result['device']] = $result['count'];
833
            } elseif (null !== $result['list_id']) {
834
                if (!isset($stats[$result['list_id']])) {
835
                    $stats[$result['list_id']] = [];
836
                }
837
838
                if (!isset($stats[$result['list_id']][$result['device']])) {
839
                    $stats[$result['list_id']][$result['device']] = (int) $result['count'];
840
                } else {
841
                    $stats[$result['list_id']][$result['device']] += (int) $result['count'];
842
                }
843
            }
844
        }
845
846
        $listCount = 0;
847
        if (!$templateEmail) {
848
            $lists     = $email->getLists();
849
            $listNames = [];
850
            foreach ($lists as $l) {
851
                $listNames[$l->getId()] = $l->getName();
852
            }
853
            $listCount = count($listNames);
854
        }
855
856
        natcasesort($devices);
857
        $chart = new BarChart(array_values($devices));
858
859
        if ($templateEmail) {
860
            // Populate the data
861
            $chart->setDataset(
862
                null,
863
                array_values($stats),
864
                0
865
            );
866
        } else {
867
            $combined = [];
868
            $key      = ($listCount > 1) ? 1 : 0;
869
            foreach ($listNames as $id => $name) {
870
                // Fill in missing devices
871
                $listStats = [];
872
                foreach ($devices as $device) {
873
                    $listStat    = (!isset($stats[$id][$device])) ? 0 : $stats[$id][$device];
874
                    $listStats[] = $listStat;
875
876
                    if (!isset($combined[$device])) {
877
                        $combined[$device] = 0;
878
                    }
879
880
                    $combined[$device] += $listStat;
881
                }
882
883
                // Populate the data
884
                $chart->setDataset(
885
                    $name,
886
                    $listStats,
887
                    $key
888
                );
889
890
                ++$key;
891
            }
892
893
            if ($listCount > 1) {
894
                $chart->setDataset(
895
                    $this->translator->trans('mautic.email.lists.combined'),
896
                    array_values($combined),
897
                    0
898
                );
899
            }
900
        }
901
902
        return $chart->render();
903
    }
904
905
    /**
906
     * @param      $email
907
     * @param bool $includeVariants
908
     * @param      $unit
909
     *
910
     * @return array
911
     */
912
    public function getEmailGeneralStats($email, $includeVariants, $unit, \DateTime $dateFrom, \DateTime $dateTo)
913
    {
914
        if (!$email instanceof Email) {
915
            $email = $this->getEntity($email);
916
        }
917
918
        $filter = [
919
            'email_id' => ($includeVariants) ? $email->getRelatedEntityIds() : [$email->getId()],
920
            'flag'     => 'all',
921
        ];
922
923
        return $this->getEmailsLineChartData($unit, $dateFrom, $dateTo, null, $filter);
924
    }
925
926
    /**
927
     * Get an array of tracked links.
928
     *
929
     * @param $emailId
930
     *
931
     * @return array
932
     */
933
    public function getEmailClickStats($emailId)
934
    {
935
        return $this->pageTrackableModel->getTrackableList('email', $emailId);
936
    }
937
938
    /**
939
     * Get the number of leads this email will be sent to.
940
     *
941
     * @param mixed $listId          Leads for a specific lead list
942
     * @param bool  $countOnly       If true, return count otherwise array of leads
943
     * @param int   $limit           Max number of leads to retrieve
944
     * @param bool  $includeVariants If false, emails sent to a variant will not be included
945
     * @param int   $minContactId    Filter by min contact ID
946
     * @param int   $maxContactId    Filter by max contact ID
947
     * @param bool  $countWithMaxMin Add min_id and max_id info to the count result
948
     * @param bool  $storeToCache    Whether to store the result to the cache
949
     *
950
     * @return int|array
951
     */
952
    public function getPendingLeads(
953
        Email $email,
954
        $listId = null,
955
        $countOnly = false,
956
        $limit = null,
957
        $includeVariants = true,
958
        $minContactId = null,
959
        $maxContactId = null,
960
        $countWithMaxMin = false,
961
        $storeToCache = true
962
    ) {
963
        $variantIds = ($includeVariants) ? $email->getRelatedEntityIds() : null;
964
        $total      = $this->getRepository()->getEmailPendingLeads(
965
            $email->getId(),
966
            $variantIds,
967
            $listId,
968
            $countOnly,
969
            $limit,
970
            $minContactId,
971
            $maxContactId,
972
            $countWithMaxMin
973
        );
974
975
        if ($storeToCache) {
976
            if ($countOnly && $countWithMaxMin) {
977
                $toStore = $total['count'];
978
            } elseif ($countOnly) {
979
                $toStore = $total;
980
            } else {
981
                $toStore = count($total);
982
            }
983
984
            $this->cacheStorageHelper->set(sprintf('%s|%s|%s', 'email', $email->getId(), 'pending'), $toStore);
985
        }
986
987
        return $total;
988
    }
989
990
    /**
991
     * @param bool $includeVariants
992
     *
993
     * @return array|int
994
     */
995
    public function getQueuedCounts(Email $email, $includeVariants = true)
996
    {
997
        $ids = ($includeVariants) ? $email->getRelatedEntityIds() : null;
998
        if (!in_array($email->getId(), $ids)) {
999
            $ids[] = $email->getId();
1000
        }
1001
1002
        $queued = (int) $this->messageQueueModel->getQueuedChannelCount('email', $ids);
1003
        $this->cacheStorageHelper->set(sprintf('%s|%s|%s', 'email', $email->getId(), 'queued'), $queued);
1004
1005
        return $queued;
1006
    }
1007
1008
    /**
1009
     * Send an email to lead lists.
1010
     *
1011
     * @param array           $lists
1012
     * @param int             $limit
1013
     * @param bool            $batch        True to process and batch all pending leads
1014
     * @param OutputInterface $output
1015
     * @param int             $minContactId
1016
     * @param int             $maxContactId
1017
     *
1018
     * @return array array(int $sentCount, int $failedCount, array $failedRecipientsByList)
1019
     */
1020
    public function sendEmailToLists(
1021
        Email $email,
1022
        $lists = null,
1023
        $limit = null,
1024
        $batch = false,
1025
        OutputInterface $output = null,
1026
        $minContactId = null,
1027
        $maxContactId = null
1028
    ) {
1029
        //get the leads
1030
        if (empty($lists)) {
1031
            $lists = $email->getLists();
1032
        }
1033
1034
        // Safety check
1035
        if ('list' !== $email->getEmailType()) {
1036
            return [0, 0, []];
1037
        }
1038
1039
        // Doesn't make sense to send unpublished emails. Probably a user error.
1040
        // @todo throw an exception in Mautic 3 here.
1041
        if (!$email->isPublished()) {
1042
            return [0, 0, []];
1043
        }
1044
1045
        $options = [
1046
            'source'        => ['email', $email->getId()],
1047
            'allowResends'  => false,
1048
            'customHeaders' => [
1049
                'Precedence' => 'Bulk',
1050
                'X-EMAIL-ID' => $email->getId(),
1051
            ],
1052
        ];
1053
1054
        $failedRecipientsByList = [];
1055
        $sentCount              = 0;
1056
        $failedCount            = 0;
1057
1058
        $progress = false;
1059
        if ($batch && $output) {
1060
            $progressCounter = 0;
1061
            $totalLeadCount  = $this->getPendingLeads($email, null, true, null, true, $minContactId, $maxContactId, false, false);
1062
            if (!$totalLeadCount) {
1063
                return [0, 0, []];
1064
            }
1065
1066
            // Broadcast send through CLI
1067
            $output->writeln("\n<info>".$email->getName().'</info>');
1068
            $progress = new ProgressBar($output, $totalLeadCount);
1069
        }
1070
1071
        foreach ($lists as $list) {
1072
            if (!$batch && null !== $limit && $limit <= 0) {
1073
                // Hit the max for this batch
1074
                break;
1075
            }
1076
1077
            $options['listId'] = $list->getId();
1078
            $leads             = $this->getPendingLeads($email, $list->getId(), false, $limit, true, $minContactId, $maxContactId, false, false);
1079
            $leadCount         = count($leads);
1080
1081
            while ($leadCount) {
1082
                $sentCount += $leadCount;
1083
1084
                if (!$batch && null != $limit) {
1085
                    // Only retrieve the difference between what has already been sent and the limit
1086
                    $limit -= $leadCount;
1087
                }
1088
1089
                $listErrors = $this->sendEmail($email, $leads, $options);
1090
1091
                if (!empty($listErrors)) {
1092
                    $listFailedCount = count($listErrors);
1093
1094
                    $sentCount -= $listFailedCount;
1095
                    $failedCount += $listFailedCount;
1096
1097
                    $failedRecipientsByList[$options['listId']] = $listErrors;
1098
                }
1099
1100
                if ($batch) {
1101
                    if ($progress) {
1102
                        $progressCounter += $leadCount;
1103
                        $progress->setProgress($progressCounter);
1104
                    }
1105
1106
                    // Get the next batch of leads
1107
                    $leads     = $this->getPendingLeads($email, $list->getId(), false, $limit, true, $minContactId, $maxContactId, false, false);
1108
                    $leadCount = count($leads);
1109
                } else {
1110
                    $leadCount = 0;
1111
                }
1112
            }
1113
        }
1114
1115
        if ($progress) {
1116
            $progress->finish();
1117
        }
1118
1119
        return [$sentCount, $failedCount, $failedRecipientsByList];
1120
    }
1121
1122
    /**
1123
     * Gets template, stats, weights, etc for an email in preparation to be sent.
1124
     *
1125
     * @param bool $includeVariants
1126
     *
1127
     * @return array
1128
     */
1129
    public function &getEmailSettings(Email $email, $includeVariants = true)
1130
    {
1131
        if (empty($this->emailSettings[$email->getId()])) {
1132
            //used to house slots so they don't have to be fetched over and over for same template
1133
            // BC for Mautic v1 templates
1134
            $slots = [];
1135
            if ($template = $email->getTemplate()) {
1136
                $slots[$template] = $this->themeHelper->getTheme($template)->getSlots('email');
1137
            }
1138
1139
            //store the settings of all the variants in order to properly disperse the emails
1140
            //set the parent's settings
1141
            $emailSettings = [
1142
                $email->getId() => [
1143
                    'template'     => $email->getTemplate(),
1144
                    'slots'        => $slots,
1145
                    'sentCount'    => $email->getSentCount(),
1146
                    'variantCount' => $email->getVariantSentCount(),
1147
                    'isVariant'    => null !== $email->getVariantStartDate(),
1148
                    'entity'       => $email,
1149
                    'translations' => $email->getTranslations(true),
1150
                    'languages'    => ['default' => $email->getId()],
1151
                ],
1152
            ];
1153
1154
            if ($emailSettings[$email->getId()]['translations']) {
1155
                // Add in the sent counts for translations of this email
1156
                /** @var Email $translation */
1157
                foreach ($emailSettings[$email->getId()]['translations'] as $translation) {
1158
                    if ($translation->isPublished()) {
1159
                        $emailSettings[$email->getId()]['sentCount'] += $translation->getSentCount();
1160
                        $emailSettings[$email->getId()]['variantCount'] += $translation->getVariantSentCount();
1161
1162
                        // Prevent empty key due to misconfiguration - pretty much ignored
1163
                        if (!$language = $translation->getLanguage()) {
1164
                            $language = 'unknown';
1165
                        }
1166
                        $core = $this->getTranslationLocaleCore($language);
1167
                        if (!isset($emailSettings[$email->getId()]['languages'][$core])) {
1168
                            $emailSettings[$email->getId()]['languages'][$core] = [];
1169
                        }
1170
                        $emailSettings[$email->getId()]['languages'][$core][$language] = $translation->getId();
1171
                    }
1172
                }
1173
            }
1174
1175
            if ($includeVariants && $email->isVariant()) {
1176
                //get a list of variants for A/B testing
1177
                $childrenVariant = $email->getVariantChildren();
1178
1179
                if (count($childrenVariant)) {
1180
                    $variantWeight = 0;
1181
                    $totalSent     = $emailSettings[$email->getId()]['variantCount'];
1182
1183
                    foreach ($childrenVariant as $child) {
1184
                        if ($child->isPublished()) {
1185
                            $useSlots = [];
1186
                            if ($template = $child->getTemplate()) {
1187
                                if (isset($slots[$template])) {
1188
                                    $useSlots = $slots[$template];
1189
                                } else {
1190
                                    $slots[$template] = $this->themeHelper->getTheme($template)->getSlots('email');
1191
                                    $useSlots         = $slots[$template];
1192
                                }
1193
                            }
1194
                            $variantSettings                = $child->getVariantSettings();
1195
                            $emailSettings[$child->getId()] = [
1196
                                'template'     => $child->getTemplate(),
1197
                                'slots'        => $useSlots,
1198
                                'sentCount'    => $child->getSentCount(),
1199
                                'variantCount' => $child->getVariantSentCount(),
1200
                                'isVariant'    => null !== $email->getVariantStartDate(),
1201
                                'weight'       => ($variantSettings['weight'] / 100),
1202
                                'entity'       => $child,
1203
                                'translations' => $child->getTranslations(true),
1204
                                'languages'    => ['default' => $child->getId()],
1205
                            ];
1206
1207
                            $variantWeight += $variantSettings['weight'];
1208
1209
                            if ($emailSettings[$child->getId()]['translations']) {
1210
                                // Add in the sent counts for translations of this email
1211
                                /** @var Email $translation */
1212
                                foreach ($emailSettings[$child->getId()]['translations'] as $translation) {
1213
                                    if ($translation->isPublished()) {
1214
                                        $emailSettings[$child->getId()]['sentCount'] += $translation->getSentCount();
1215
                                        $emailSettings[$child->getId()]['variantCount'] += $translation->getVariantSentCount();
1216
1217
                                        // Prevent empty key due to misconfiguration - pretty much ignored
1218
                                        if (!$language = $translation->getLanguage()) {
1219
                                            $language = 'unknown';
1220
                                        }
1221
                                        $core = $this->getTranslationLocaleCore($language);
1222
                                        if (!isset($emailSettings[$child->getId()]['languages'][$core])) {
1223
                                            $emailSettings[$child->getId()]['languages'][$core] = [];
1224
                                        }
1225
                                        $emailSettings[$child->getId()]['languages'][$core][$language] = $translation->getId();
1226
                                    }
1227
                                }
1228
                            }
1229
1230
                            $totalSent += $emailSettings[$child->getId()]['variantCount'];
1231
                        }
1232
                    }
1233
1234
                    //set parent weight
1235
                    $emailSettings[$email->getId()]['weight'] = ((100 - $variantWeight) / 100);
1236
                } else {
1237
                    $emailSettings[$email->getId()]['weight'] = 1;
1238
                }
1239
            }
1240
1241
            $this->emailSettings[$email->getId()] = $emailSettings;
1242
        }
1243
1244
        if ($includeVariants && $email->isVariant()) {
1245
            //now find what percentage of current leads should receive the variants
1246
            if (!isset($totalSent)) {
1247
                $totalSent = 0;
1248
                foreach ($this->emailSettings[$email->getId()] as $details) {
1249
                    $totalSent += $details['variantCount'];
1250
                }
1251
            }
1252
1253
            foreach ($this->emailSettings[$email->getId()] as &$details) {
1254
                // Determine the deficit for email ordering
1255
                if ($totalSent) {
1256
                    $details['weight_deficit'] = $details['weight'] - ($details['variantCount'] / $totalSent);
1257
                    $details['send_weight']    = ($details['weight'] - ($details['variantCount'] / $totalSent)) + $details['weight'];
1258
                } else {
1259
                    $details['weight_deficit'] = $details['weight'];
1260
                    $details['send_weight']    = $details['weight'];
1261
                }
1262
            }
1263
1264
            // Reorder according to send_weight so that campaigns which currently send one at a time alternate
1265
            uasort($this->emailSettings[$email->getId()], function ($a, $b) {
1266
                if ($a['weight_deficit'] === $b['weight_deficit']) {
1267
                    if ($a['variantCount'] === $b['variantCount']) {
1268
                        return 0;
1269
                    }
1270
1271
                    // if weight is the same - sort by least number sent
1272
                    return ($a['variantCount'] < $b['variantCount']) ? -1 : 1;
1273
                }
1274
1275
                // sort by the one with the greatest deficit first
1276
                return ($a['weight_deficit'] > $b['weight_deficit']) ? -1 : 1;
1277
            });
1278
        }
1279
1280
        return $this->emailSettings[$email->getId()];
1281
    }
1282
1283
    /**
1284
     * Send an email to lead(s).
1285
     *
1286
     * @param $email
1287
     * @param $leads
1288
     * @param $options = array()
1289
     *                  array source array('model', 'id')
1290
     *                  array emailSettings
1291
     *                  int   listId
1292
     *                  bool  allowResends     If false, exact emails (by id) already sent to the lead will not be resent
1293
     *                  bool  ignoreDNC        If true, emails listed in the do not contact table will still get the email
1294
     *                  array assetAttachments Array of optional Asset IDs to attach
1295
     *
1296
     * @return mixed
1297
     *
1298
     * @throws \Doctrine\ORM\ORMException
1299
     */
1300
    public function sendEmail(Email $email, $leads, $options = [])
1301
    {
1302
        $listId              = ArrayHelper::getValue('listId', $options);
1303
        $ignoreDNC           = ArrayHelper::getValue('ignoreDNC', $options, false);
1304
        $tokens              = ArrayHelper::getValue('tokens', $options, []);
1305
        $assetAttachments    = ArrayHelper::getValue('assetAttachments', $options, []);
1306
        $customHeaders       = ArrayHelper::getValue('customHeaders', $options, []);
1307
        $emailType           = ArrayHelper::getValue('email_type', $options, '');
1308
        $isMarketing         = (in_array($emailType, ['marketing']) || !empty($listId));
1309
        $emailAttempts       = ArrayHelper::getValue('email_attempts', $options, 3);
1310
        $emailPriority       = ArrayHelper::getValue('email_priority', $options, MessageQueue::PRIORITY_NORMAL);
1311
        $messageQueue        = ArrayHelper::getValue('resend_message_queue', $options);
1312
        $returnErrorMessages = ArrayHelper::getValue('return_errors', $options, false);
1313
        $channel             = ArrayHelper::getValue('channel', $options);
1314
        $dncAsError          = ArrayHelper::getValue('dnc_as_error', $options, false);
1315
        $errors              = [];
1316
1317
        if (empty($channel)) {
1318
            $channel = (isset($options['source'])) ? $options['source'] : [];
1319
        }
1320
1321
        if (!$email->getId()) {
1322
            return false;
1323
        }
1324
1325
        // Ensure $sendTo is indexed by lead ID
1326
        $leadIds     = [];
1327
        $singleEmail = false;
1328
        if (isset($leads['id'])) {
1329
            $singleEmail           = $leads['id'];
1330
            $leadIds[$leads['id']] = $leads['id'];
1331
            $leads                 = [$leads['id'] => $leads];
1332
            $sendTo                = $leads;
1333
        } else {
1334
            $sendTo = [];
1335
            foreach ($leads as $lead) {
1336
                $sendTo[$lead['id']]  = $lead;
1337
                $leadIds[$lead['id']] = $lead['id'];
1338
            }
1339
        }
1340
1341
        /** @var \Mautic\EmailBundle\Entity\EmailRepository $emailRepo */
1342
        $emailRepo = $this->getRepository();
1343
1344
        //get email settings such as templates, weights, etc
1345
        $emailSettings = &$this->getEmailSettings($email);
1346
1347
        if (!$ignoreDNC) {
1348
            $dnc = $emailRepo->getDoNotEmailList($leadIds);
1349
1350
            if (!empty($dnc)) {
1351
                foreach ($dnc as $removeMeId => $removeMeEmail) {
1352
                    if ($dncAsError) {
1353
                        $errors[$removeMeId] = $this->translator->trans('mautic.email.dnc');
1354
                    }
1355
                    unset($sendTo[$removeMeId]);
1356
                    unset($leadIds[$removeMeId]);
1357
                }
1358
            }
1359
        }
1360
1361
        // Process frequency rules for email
1362
        if ($isMarketing && count($sendTo)) {
1363
            $campaignEventId = (is_array($channel) && !empty($channel) && 'campaign.event' === $channel[0] && !empty($channel[1])) ? $channel[1]
1364
                : null;
1365
            $this->messageQueueModel->processFrequencyRules(
1366
                $sendTo,
1367
                'email',
1368
                $email->getId(),
1369
                $campaignEventId,
1370
                $emailAttempts,
1371
                $emailPriority,
1372
                $messageQueue
1373
            );
1374
        }
1375
1376
        //get a count of leads
1377
        $count = count($sendTo);
1378
1379
        //no one to send to so bail or if marketing email from a campaign has been put in a queue
1380
        if (empty($count)) {
1381
            if ($returnErrorMessages) {
1382
                return $singleEmail && isset($errors[$singleEmail]) ? $errors[$singleEmail] : $errors;
1383
            }
1384
1385
            return $singleEmail ? true : $errors;
1386
        }
1387
1388
        // Hydrate contacts with company profile fields
1389
        $this->getContactCompanies($sendTo);
1390
1391
        foreach ($emailSettings as $eid => $details) {
1392
            if (isset($details['send_weight'])) {
1393
                $emailSettings[$eid]['limit'] = ceil($count * $details['send_weight']);
1394
            } else {
1395
                $emailSettings[$eid]['limit'] = $count;
1396
            }
1397
        }
1398
1399
        // Randomize the contacts for statistic purposes
1400
        shuffle($sendTo);
1401
1402
        // Organize the contacts according to the variant and translation they are to receive
1403
        $groupedContactsByEmail = [];
1404
        $offset                 = 0;
1405
        foreach ($emailSettings as $eid => $details) {
1406
            if (empty($details['limit'])) {
1407
                continue;
1408
            }
1409
            $groupedContactsByEmail[$eid] = [];
1410
            if ($details['limit']) {
1411
                // Take a chunk of contacts based on variant weights
1412
                if ($batchContacts = array_slice($sendTo, $offset, $details['limit'])) {
1413
                    $offset += $details['limit'];
1414
1415
                    // Group contacts by preferred locale
1416
                    foreach ($batchContacts as $key => $contact) {
1417
                        if (!empty($contact['preferred_locale'])) {
1418
                            $locale     = $contact['preferred_locale'];
1419
                            $localeCore = $this->getTranslationLocaleCore($locale);
1420
1421
                            if (isset($details['languages'][$localeCore])) {
1422
                                if (isset($details['languages'][$localeCore][$locale])) {
1423
                                    // Exact match
1424
                                    $translatedId                                  = $details['languages'][$localeCore][$locale];
1425
                                    $groupedContactsByEmail[$eid][$translatedId][] = $contact;
1426
                                } else {
1427
                                    // Grab the closest match
1428
                                    $bestMatch                                     = array_keys($details['languages'][$localeCore])[0];
1429
                                    $translatedId                                  = $details['languages'][$localeCore][$bestMatch];
1430
                                    $groupedContactsByEmail[$eid][$translatedId][] = $contact;
1431
                                }
1432
1433
                                unset($batchContacts[$key]);
1434
                            }
1435
                        }
1436
                    }
1437
1438
                    // If there are any contacts left over, assign them to the default
1439
                    if (count($batchContacts)) {
1440
                        $translatedId                                = $details['languages']['default'];
1441
                        $groupedContactsByEmail[$eid][$translatedId] = $batchContacts;
1442
                    }
1443
                }
1444
            }
1445
        }
1446
1447
        foreach ($groupedContactsByEmail as $parentId => $translatedEmails) {
1448
            $useSettings = $emailSettings[$parentId];
1449
            foreach ($translatedEmails as $translatedId => $contacts) {
1450
                $emailEntity = ($translatedId === $parentId) ? $useSettings['entity'] : $useSettings['translations'][$translatedId];
1451
1452
                $this->sendModel->setEmail($emailEntity, $channel, $customHeaders, $assetAttachments)
1453
                    ->setListId($listId);
1454
1455
                foreach ($contacts as $contact) {
1456
                    try {
1457
                        $this->sendModel->setContact($contact, $tokens)
1458
                            ->send();
1459
1460
                        // Update $emailSetting so campaign a/b tests are handled correctly
1461
                        ++$emailSettings[$parentId]['sentCount'];
1462
1463
                        if (!empty($emailSettings[$parentId]['isVariant'])) {
1464
                            ++$emailSettings[$parentId]['variantCount'];
1465
                        }
1466
                    } catch (FailedToSendToContactException $exception) {
1467
                        // move along to the next contact
1468
                    }
1469
                }
1470
            }
1471
        }
1472
1473
        // Flush the queue and store pending email stats
1474
        $this->sendModel->finalFlush();
1475
1476
        // Get the errors to return
1477
1478
        // Don't use array_merge or it will reset contact ID based keys
1479
        $errorMessages  = $errors + $this->sendModel->getErrors();
1480
        $failedContacts = $this->sendModel->getFailedContacts();
1481
1482
        // Get sent counts to update email stats
1483
        $sentCounts = $this->sendModel->getSentCounts();
1484
1485
        // Reset the model for the next send
1486
        $this->sendModel->reset();
1487
1488
        // Update sent counts
1489
        foreach ($sentCounts as $emailId => $count) {
1490
            // Retry a few times in case of deadlock errors
1491
            $strikes = 3;
1492
            while ($strikes >= 0) {
1493
                try {
1494
                    $this->getRepository()->upCount($emailId, 'sent', $count, $emailSettings[$emailId]['isVariant']);
1495
                    break;
1496
                } catch (\Exception $exception) {
1497
                    error_log($exception);
1498
                }
1499
                --$strikes;
1500
            }
1501
        }
1502
1503
        unset($emailSettings, $options, $sendTo);
1504
1505
        $success = empty($failedContacts);
1506
        if (!$success && $returnErrorMessages) {
1507
            return $singleEmail ? $errorMessages[$singleEmail] : $errorMessages;
1508
        }
1509
1510
        return $singleEmail ? $success : $failedContacts;
1511
    }
1512
1513
    /**
1514
     * Send an email to lead(s).
1515
     *
1516
     * @param array|int $users
1517
     * @param array     $lead
1518
     * @param bool      $saveStat
1519
     *
1520
     * @return mixed
1521
     *
1522
     * @throws \Doctrine\ORM\ORMException
1523
     */
1524
    public function sendEmailToUser(
1525
        Email $email,
1526
        $users,
1527
        array $lead = null,
1528
        array $tokens = [],
1529
        array $assetAttachments = [],
1530
        $saveStat = false,
1531
        array $to = [],
1532
        array $cc = [],
1533
        array $bcc = []
1534
    ) {
1535
        if (!$emailId = $email->getId()) {
1536
            return false;
1537
        }
1538
1539
        // In case only user ID was provided
1540
        if (!is_array($users)) {
1541
            $users = [['id' => $users]];
1542
        }
1543
1544
        // Get email settings
1545
        $emailSettings = &$this->getEmailSettings($email, false);
1546
1547
        // No one to send to so bail
1548
        if (empty($users) && empty($to)) {
1549
            return false;
1550
        }
1551
1552
        $mailer            = $this->mailHelper->getMailer();
1553
        if (!isset($lead['companies'])) {
1554
            $lead['companies'] = $this->companyModel->getRepository()->getCompaniesByLeadId($lead['id']);
1555
        }
1556
        $mailer->setLead($lead, true);
1557
        $mailer->setTokens($tokens);
1558
        $mailer->setEmail($email, false, $emailSettings[$emailId]['slots'], $assetAttachments, (!$saveStat));
1559
        $mailer->setCc($cc);
1560
        $mailer->setBcc($bcc);
1561
1562
        $errors = [];
1563
1564
        $firstMail = true;
1565
        foreach ($to as $toAddress) {
1566
            $idHash = uniqid();
1567
            $mailer->setIdHash($idHash, $saveStat);
1568
1569
            if (!$mailer->addTo($toAddress)) {
1570
                $errors[] = "{$toAddress}: ".$this->translator->trans('mautic.email.bounce.reason.bad_email');
1571
                continue;
1572
            }
1573
1574
            if (!$mailer->queue(true)) {
1575
                $errorArray = $mailer->getErrors();
1576
                unset($errorArray['failures']);
1577
                $errors[] = "{$toAddress}: ".implode('; ', $errorArray);
1578
            }
1579
1580
            if ($saveStat) {
1581
                $saveEntities[] = $mailer->createEmailStat(false, $toAddress);
1582
            }
1583
1584
            // If this is the first message, flush the queue. This process clears the cc and bcc.
1585
            if (true === $firstMail) {
1586
                try {
1587
                    $this->flushQueue($mailer);
1588
                } catch (EmailCouldNotBeSentException $e) {
1589
                    $errors[] = $e->getMessage();
1590
                }
1591
                $firstMail = false;
1592
            }
1593
        }
1594
1595
        foreach ($users as $user) {
1596
            $idHash = uniqid();
1597
            $mailer->setIdHash($idHash, $saveStat);
1598
1599
            if (!is_array($user)) {
1600
                $id   = $user;
1601
                $user = ['id' => $id];
1602
            } else {
1603
                $id = $user['id'];
1604
            }
1605
1606
            if (!isset($user['email'])) {
1607
                $userEntity = $this->userModel->getEntity($id);
1608
1609
                if (null === $userEntity) {
1610
                    continue;
1611
                }
1612
1613
                $user['email']     = $userEntity->getEmail();
1614
                $user['firstname'] = $userEntity->getFirstName();
1615
                $user['lastname']  = $userEntity->getLastName();
1616
            }
1617
1618
            if (!$mailer->setTo($user['email'], $user['firstname'].' '.$user['lastname'])) {
1619
                $errors[] = "{$user['email']}: ".$this->translator->trans('mautic.email.bounce.reason.bad_email');
1620
                continue;
1621
            }
1622
1623
            if (!$mailer->queue(true)) {
1624
                $errorArray = $mailer->getErrors();
1625
                unset($errorArray['failures']);
1626
                $errors[] = "{$user['email']}: ".implode('; ', $errorArray);
1627
            }
1628
1629
            if ($saveStat) {
1630
                $saveEntities[] = $mailer->createEmailStat(false, $user['email']);
1631
            }
1632
1633
            // If this is the first message, flush the queue. This process clears the cc and bcc.
1634
            if (true === $firstMail) {
1635
                try {
1636
                    $this->flushQueue($mailer);
1637
                } catch (EmailCouldNotBeSentException $e) {
1638
                    $errors[] = $e->getMessage();
1639
                }
1640
                $firstMail = false;
1641
            }
1642
        }
1643
1644
        try {
1645
            $this->flushQueue($mailer);
1646
        } catch (EmailCouldNotBeSentException $e) {
1647
            $errors[] = $e->getMessage();
1648
        }
1649
1650
        if (isset($saveEntities)) {
1651
            $this->getStatRepository()->saveEntities($saveEntities);
1652
        }
1653
1654
        //save some memory
1655
        unset($mailer);
1656
1657
        return $errors;
1658
    }
1659
1660
    /**
1661
     * @throws EmailCouldNotBeSentException
1662
     */
1663
    private function flushQueue(MailHelper $mailer): void
1664
    {
1665
        if (!$mailer->flushQueue()) {
1666
            $errorArray = $mailer->getErrors();
1667
            unset($errorArray['failures']);
1668
1669
            throw new EmailCouldNotBeSentException(implode('; ', $errorArray));
1670
        }
1671
    }
1672
1673
    /**
1674
     * Dispatches EmailSendEvent so you could get tokens form it or tokenized content.
1675
     *
1676
     * @param string $idHash
1677
     *
1678
     * @return EmailSendEvent
1679
     */
1680
    public function dispatchEmailSendEvent(Email $email, array $leadFields = [], $idHash = null, array $tokens = [])
1681
    {
1682
        $event = new EmailSendEvent(
1683
            null,
1684
            [
1685
                'content'      => $email->getCustomHtml(),
1686
                'email'        => $email,
1687
                'idHash'       => $idHash,
1688
                'tokens'       => $tokens,
1689
                'internalSend' => true,
1690
                'lead'         => $leadFields,
1691
            ]
1692
        );
1693
1694
        $this->dispatcher->dispatch(EmailEvents::EMAIL_ON_DISPLAY, $event);
1695
1696
        return $event;
1697
    }
1698
1699
    /**
1700
     * @param      $comments
1701
     * @param int  $reason
1702
     * @param bool $flush
1703
     *
1704
     * @return bool|DoNotContact
1705
     */
1706
    public function setDoNotContact(Stat $stat, $comments, $reason = DoNotContact::BOUNCED, $flush = true)
1707
    {
1708
        $lead = $stat->getLead();
1709
1710
        if ($lead instanceof Lead) {
1711
            $email   = $stat->getEmail();
1712
            $channel = ($email) ? ['email' => $email->getId()] : 'email';
1713
1714
            return $this->doNotContact->addDncForContact($lead->getId(), $channel, $reason, $comments, $flush);
1715
        }
1716
1717
        return false;
1718
    }
1719
1720
    /**
1721
     * Remove a Lead's EMAIL DNC entry.
1722
     *
1723
     * @param string $email
1724
     */
1725
    public function removeDoNotContact($email)
1726
    {
1727
        /** @var \Mautic\LeadBundle\Entity\LeadRepository $leadRepo */
1728
        $leadRepo = $this->em->getRepository('MauticLeadBundle:Lead');
1729
        $leadId   = (array) $leadRepo->getLeadByEmail($email, true);
1730
1731
        /** @var \Mautic\LeadBundle\Entity\Lead[] $leads */
1732
        $leads = [];
1733
1734
        foreach ($leadId as $lead) {
1735
            $leads[] = $leadRepo->getEntity($lead['id']);
1736
        }
1737
1738
        foreach ($leads as $lead) {
1739
            $this->doNotContact->removeDncForContact($lead->getId(), 'email');
1740
        }
1741
    }
1742
1743
    /**
1744
     * @param        $email
1745
     * @param int    $reason
1746
     * @param string $comments
1747
     * @param bool   $flush
1748
     * @param null   $leadId
1749
     *
1750
     * @return array
1751
     */
1752
    public function setEmailDoNotContact($email, $reason = DoNotContact::BOUNCED, $comments = '', $flush = true, $leadId = null)
1753
    {
1754
        /** @var \Mautic\LeadBundle\Entity\LeadRepository $leadRepo */
1755
        $leadRepo = $this->em->getRepository('MauticLeadBundle:Lead');
1756
1757
        if (null === $leadId) {
1758
            $leadId = (array) $leadRepo->getLeadByEmail($email, true);
1759
        } elseif (!is_array($leadId)) {
1760
            $leadId = [$leadId];
1761
        }
1762
1763
        $dnc = [];
1764
        foreach ($leadId as $lead) {
1765
            $dnc[] = $this->doNotContact->addDncForContact(
1766
                $this->em->getReference('MauticLeadBundle:Lead', $lead),
1767
                'email',
1768
                $reason,
1769
                $comments,
1770
                $flush
1771
            );
1772
        }
1773
1774
        return $dnc;
1775
    }
1776
1777
    /**
1778
     * Get the settings for a monitored mailbox or false if not enabled.
1779
     *
1780
     * @param $bundleKey
1781
     * @param $folderKey
1782
     *
1783
     * @return bool|array
1784
     */
1785
    public function getMonitoredMailbox($bundleKey, $folderKey)
1786
    {
1787
        if ($this->mailboxHelper->isConfigured($bundleKey, $folderKey)) {
1788
            return $this->mailboxHelper->getMailboxSettings();
1789
        }
1790
1791
        return false;
1792
    }
1793
1794
    /**
1795
     * Joins the email table and limits created_by to currently logged in user.
1796
     */
1797
    public function limitQueryToCreator(QueryBuilder &$q)
1798
    {
1799
        $q->join('t', MAUTIC_TABLE_PREFIX.'emails', 'e', 'e.id = t.email_id')
1800
            ->andWhere('e.created_by = :userId')
1801
            ->setParameter('userId', $this->userHelper->getUser()->getId());
1802
    }
1803
1804
    /**
1805
     * Get line chart data of emails sent and read.
1806
     *
1807
     * @param          $reason
1808
     * @param          $canViewOthers
1809
     * @param int|null $companyId
1810
     * @param int|null $campaignId
1811
     * @param int|null $segmentId
1812
     *
1813
     * @return array
1814
     */
1815
    public function getEmailsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, array $filter = [], $canViewOthers = true)
1816
    {
1817
        $datasets   = ArrayHelper::pickValue('dataset', $filter, []);
1818
        $flag       = ArrayHelper::pickValue('flag', $filter);
1819
        $companyId  = ArrayHelper::pickValue('companyId', $filter);
1820
        $campaignId = ArrayHelper::pickValue('campaignId', $filter);
1821
        $segmentId  = ArrayHelper::pickValue('segmentId', $filter);
1822
        $chart      = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
1823
        $query      = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo, $unit);
1824
1825
        $query->setGeneratedColumnProvider($this->generatedColumnsProvider);
1826
1827
        if ('sent_and_opened_and_failed' == $flag || 'all' == $flag || 'sent_and_opened' == $flag || !$flag || in_array('sent', $datasets)) {
1828
            $q = $query->prepareTimeDataQuery('email_stats', 'date_sent', $filter);
1829
            if (!$canViewOthers) {
1830
                $this->limitQueryToCreator($q);
1831
            }
1832
            $this->addCompanyFilter($q, $companyId);
1833
            $this->addCampaignFilter($q, $campaignId);
1834
            $this->addSegmentFilter($q, $segmentId);
1835
            $data = $query->loadAndBuildTimeData($q);
1836
            $chart->setDataset($this->translator->trans('mautic.email.sent.emails'), $data);
1837
        }
1838
1839
        if ('sent_and_opened_and_failed' == $flag || 'all' == $flag || 'sent_and_opened' == $flag || 'opened' == $flag || in_array('opened', $datasets)) {
1840
            $q = $query->prepareTimeDataQuery('email_stats', 'date_read', $filter);
1841
            if (!$canViewOthers) {
1842
                $this->limitQueryToCreator($q);
1843
            }
1844
            $this->addCompanyFilter($q, $companyId);
1845
            $this->addCampaignFilter($q, $campaignId);
1846
            $this->addSegmentFilter($q, $segmentId);
1847
            $data = $query->loadAndBuildTimeData($q);
1848
            $chart->setDataset($this->translator->trans('mautic.email.read.emails'), $data);
1849
        }
1850
1851
        if ('sent_and_opened_and_failed' == $flag || 'all' == $flag || 'failed' == $flag || in_array('failed', $datasets)) {
1852
            $q = $query->prepareTimeDataQuery('email_stats', 'date_sent', $filter);
1853
            if (!$canViewOthers) {
1854
                $this->limitQueryToCreator($q);
1855
            }
1856
            $q->andWhere($q->expr()->eq('t.is_failed', ':true'))
1857
                ->setParameter('true', true, 'boolean');
1858
            $this->addCompanyFilter($q, $companyId);
1859
            $this->addCampaignFilter($q, $campaignId);
1860
            $this->addSegmentFilter($q, $segmentId);
1861
            $data = $query->loadAndBuildTimeData($q);
1862
            $chart->setDataset($this->translator->trans('mautic.email.failed.emails'), $data);
1863
        }
1864
1865
        if ('all' == $flag || 'clicked' == $flag || in_array('clicked', $datasets)) {
1866
            $q = $query->prepareTimeDataQuery('page_hits', 'date_hit', []);
1867
1868
            if (null !== $segmentId) {
1869
                $q->innerJoin('t', '(SELECT DISTINCT email_id, lead_id FROM '.MAUTIC_TABLE_PREFIX.'email_stats WHERE list_id = :segmentId)', 'es', 't.source_id = es.email_id');
1870
                $q->setParameter('segmentId', $segmentId);
1871
            }
1872
1873
            $q->andWhere('t.source = :source');
1874
            $q->setParameter('source', 'email');
1875
1876
            if (isset($filter['email_id'])) {
1877
                if (is_array($filter['email_id'])) {
1878
                    $q->andWhere('t.source_id IN (:email_ids)');
1879
                    $q->setParameter('email_ids', $filter['email_id'], \Doctrine\DBAL\Connection::PARAM_INT_ARRAY);
1880
                } else {
1881
                    $q->andWhere('t.source_id = :email_id');
1882
                    $q->setParameter('email_id', $filter['email_id']);
1883
                }
1884
            }
1885
1886
            if (!$canViewOthers) {
1887
                $this->limitQueryToCreator($q);
1888
            }
1889
            $this->addCompanyFilter($q, $companyId);
1890
            $this->addCampaignFilterForEmailSource($q, $campaignId);
1891
            $this->addSegmentFilter($q, $segmentId, 'es');
1892
            $data = $query->loadAndBuildTimeData($q);
1893
1894
            $chart->setDataset($this->translator->trans('mautic.email.clicked'), $data);
1895
        }
1896
1897
        if ('all' == $flag || 'unsubscribed' == $flag || in_array('unsubscribed', $datasets)) {
1898
            $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::UNSUBSCRIBED, $canViewOthers, $companyId, $campaignId, $segmentId);
1899
            $chart->setDataset($this->translator->trans('mautic.email.unsubscribed'), $data);
1900
        }
1901
1902
        if ('all' == $flag || 'bounced' == $flag || in_array('bounced', $datasets)) {
1903
            $data = $this->getDncLineChartDataset($query, $filter, DoNotContact::BOUNCED, $canViewOthers, $companyId, $campaignId, $segmentId);
1904
            $chart->setDataset($this->translator->trans('mautic.email.bounced'), $data);
1905
        }
1906
1907
        return $chart->render();
1908
    }
1909
1910
    /**
1911
     * Modifies the line chart query for the DNC.
1912
     *
1913
     * @param          $reason
1914
     * @param          $canViewOthers
1915
     * @param int|null $companyId
1916
     * @param int|null $campaignId
1917
     * @param int|null $segmentId
1918
     *
1919
     * @return array
1920
     */
1921
    public function getDncLineChartDataset(ChartQuery &$query, array $filter, $reason, $canViewOthers, $companyId = null, $campaignId = null, $segmentId = null)
1922
    {
1923
        $dncFilter = isset($filter['email_id']) ? ['channel_id' => $filter['email_id']] : [];
1924
        $q         = $query->prepareTimeDataQuery('lead_donotcontact', 'date_added', $dncFilter);
1925
        $q->andWhere('t.channel = :channel')
1926
            ->setParameter('channel', 'email')
1927
            ->andWhere($q->expr()->eq('t.reason', ':reason'))
1928
            ->setParameter('reason', $reason);
1929
1930
        $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');
1931
1932
        if (!$canViewOthers) {
1933
            $this->limitQueryToCreator($q);
1934
        }
1935
        $this->addCompanyFilter($q, $companyId);
1936
        $this->addCampaignFilter($q, $campaignId, 'es');
1937
        $this->addSegmentFilter($q, $segmentId, 'es');
1938
1939
        return $data = $query->loadAndBuildTimeData($q);
1940
    }
1941
1942
    /**
1943
     * @param int|null $companyId
1944
     * @param string   $fromAlias
1945
     */
1946
    private function addCompanyFilter(QueryBuilder $q, $companyId = null, $fromAlias = 't')
1947
    {
1948
        if (!$companyId) {
1949
            return;
1950
        }
1951
1952
        $sb = $this->em->getConnection()->createQueryBuilder();
1953
1954
        $sb->select('null')
1955
            ->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
1956
            ->where(
1957
                $sb->expr()->andX(
1958
                    $sb->expr()->eq('cl.company_id', ':companyId'),
1959
                    $sb->expr()->eq('cl.lead_id', $fromAlias.'.lead_id')
1960
                )
1961
            );
1962
1963
        $q->andWhere(
1964
            sprintf('EXISTS (%s)', $sb->getSql())
1965
        )->setParameter('companyId', $companyId);
1966
    }
1967
1968
    /**
1969
     * @param int|null $campaignId
1970
     * @param string   $fromAlias
1971
     */
1972
    private function addCampaignFilter(QueryBuilder $q, $campaignId = null, $fromAlias = 't')
1973
    {
1974
        if ($campaignId) {
1975
            $q->innerJoin($fromAlias, '(SELECT DISTINCT event_id, lead_id FROM '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_log WHERE campaign_id = :campaignId)', 'clel', $fromAlias.'.source_id = clel.event_id AND '.$fromAlias.'.source = "campaign.event" AND '.$fromAlias.'.lead_id = clel.lead_id')
1976
                ->setParameter('campaignId', $campaignId);
1977
        }
1978
    }
1979
1980
    /**
1981
     * @param int|null $campaignId
1982
     * @param string   $fromAlias
1983
     */
1984
    private function addCampaignFilterForEmailSource(QueryBuilder $q, $campaignId = null, $fromAlias = 't')
1985
    {
1986
        if ($campaignId) {
1987
            $q->innerJoin($fromAlias, '(SELECT DISTINCT channel_id, lead_id FROM '.MAUTIC_TABLE_PREFIX.'campaign_lead_event_log WHERE campaign_id = :campaignId AND channel = "email")', 'clel', $fromAlias.'.source_id = clel.channel_id AND '.$fromAlias.'.source = "email" AND '.$fromAlias.'.lead_id = clel.lead_id')
1988
                ->setParameter('campaignId', $campaignId);
1989
        }
1990
    }
1991
1992
    /**
1993
     * @param int|null $segmentId
1994
     * @param string   $fromAlias
1995
     */
1996
    private function addSegmentFilter(QueryBuilder $q, $segmentId = null, $fromAlias = 't')
1997
    {
1998
        if ($segmentId) {
1999
            $sb = $this->em->getConnection()->createQueryBuilder();
2000
2001
            $sb->select('null')
2002
                ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll')
2003
                ->where(
2004
                    $sb->expr()->andX(
2005
                        $sb->expr()->eq('lll.leadlist_id', ':segmentId'),
2006
                        $sb->expr()->eq('lll.lead_id', $fromAlias.'.lead_id'),
2007
                        $sb->expr()->eq('lll.manually_removed', 0)
2008
                    )
2009
                );
2010
2011
            $q->andWhere(
2012
                sprintf('EXISTS (%s)', $sb->getSql())
2013
            )->setParameter('segmentId', $segmentId);
2014
        }
2015
    }
2016
2017
    /**
2018
     * Get pie chart data of ignored vs opened emails.
2019
     *
2020
     * @param string $dateFrom
2021
     * @param string $dateTo
2022
     * @param array  $filters
2023
     * @param bool   $canViewOthers
2024
     *
2025
     * @return array
2026
     */
2027
    public function getIgnoredVsReadPieChartData($dateFrom, $dateTo, $filters = [], $canViewOthers = true)
2028
    {
2029
        $chart = new PieChart();
2030
        $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2031
2032
        $readFilters                = $filters;
2033
        $readFilters['is_read']     = true;
2034
        $failedFilters              = $filters;
2035
        $failedFilters['is_failed'] = true;
2036
2037
        $sentQ   = $query->getCountQuery('email_stats', 'id', 'date_sent', $filters);
2038
        $readQ   = $query->getCountQuery('email_stats', 'id', 'date_sent', $readFilters);
2039
        $failedQ = $query->getCountQuery('email_stats', 'id', 'date_sent', $failedFilters);
2040
2041
        if (!$canViewOthers) {
2042
            $this->limitQueryToCreator($sentQ);
2043
            $this->limitQueryToCreator($readQ);
2044
            $this->limitQueryToCreator($failedQ);
2045
        }
2046
2047
        $sent   = $query->fetchCount($sentQ);
2048
        $read   = $query->fetchCount($readQ);
2049
        $failed = $query->fetchCount($failedQ);
2050
2051
        $chart->setDataset($this->translator->trans('mautic.email.graph.pie.ignored.read.failed.ignored'), ($sent - $read - $failed));
2052
        $chart->setDataset($this->translator->trans('mautic.email.graph.pie.ignored.read.failed.read'), $read);
2053
        $chart->setDataset($this->translator->trans('mautic.email.graph.pie.ignored.read.failed.failed'), $failed);
2054
2055
        return $chart->render();
2056
    }
2057
2058
    /**
2059
     * Get pie chart data of ignored vs opened emails.
2060
     *
2061
     * @param $dateFrom
2062
     * @param $dateTo
2063
     *
2064
     * @return array
2065
     */
2066
    public function getDeviceGranularityPieChartData($dateFrom, $dateTo)
2067
    {
2068
        $chart = new PieChart();
2069
2070
        $deviceStats = $this->getStatDeviceRepository()->getDeviceStats(
2071
            null,
2072
            $dateFrom,
2073
            $dateTo
2074
        );
2075
2076
        if (empty($deviceStats)) {
2077
            $deviceStats[] = [
2078
                'count'   => 0,
2079
                'device'  => $this->translator->trans('mautic.report.report.noresults'),
2080
                'list_id' => 0,
2081
            ];
2082
        }
2083
2084
        foreach ($deviceStats as $device) {
2085
            $chart->setDataset(
2086
                ($device['device']) ? $device['device'] : $this->translator->trans('mautic.core.unknown'),
2087
                $device['count']
2088
            );
2089
        }
2090
2091
        return $chart->render();
2092
    }
2093
2094
    /**
2095
     * Get a list of emails in a date range, grouped by a stat date count.
2096
     *
2097
     * @param int       $limit
2098
     * @param \DateTime $dateFrom
2099
     * @param \DateTime $dateTo
2100
     * @param array     $filters
2101
     * @param array     $options
2102
     *
2103
     * @return array
2104
     */
2105
    public function getEmailStatList($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $options = [])
2106
    {
2107
        $canViewOthers = empty($options['canViewOthers']) ? false : $options['canViewOthers'];
2108
        $q             = $this->em->getConnection()->createQueryBuilder();
2109
        $q->select('COUNT(DISTINCT t.id) AS count, e.id, e.name')
2110
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 't')
2111
            ->join('t', MAUTIC_TABLE_PREFIX.'emails', 'e', 'e.id = t.email_id')
2112
            ->orderBy('count', 'DESC')
2113
            ->groupBy('e.id')
2114
            ->setMaxResults($limit);
2115
2116
        if (!$canViewOthers) {
2117
            $q->andWhere('e.created_by = :userId')
2118
                ->setParameter('userId', $this->userHelper->getUser()->getId());
2119
        }
2120
2121
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2122
        $chartQuery->applyFilters($q, $filters);
2123
2124
        if (isset($options['groupBy']) && 'sends' == $options['groupBy']) {
2125
            $chartQuery->applyDateFilters($q, 'date_sent');
2126
        }
2127
2128
        if (isset($options['groupBy']) && 'reads' == $options['groupBy']) {
2129
            $chartQuery->applyDateFilters($q, 'date_read');
2130
        }
2131
2132
        return $q->execute()->fetchAll();
2133
    }
2134
2135
    /**
2136
     * Get a list of emails in a date range.
2137
     *
2138
     * @param int       $limit
2139
     * @param \DateTime $dateFrom
2140
     * @param \DateTime $dateTo
2141
     * @param array     $filters
2142
     * @param array     $options
2143
     *
2144
     * @return array
2145
     */
2146
    public function getEmailList($limit = 10, \DateTime $dateFrom = null, \DateTime $dateTo = null, $filters = [], $options = [])
2147
    {
2148
        $canViewOthers = empty($options['canViewOthers']) ? false : $options['canViewOthers'];
2149
        $q             = $this->em->getConnection()->createQueryBuilder();
2150
        $q->select('t.id, t.name, t.date_added, t.date_modified')
2151
            ->from(MAUTIC_TABLE_PREFIX.'emails', 't')
2152
            ->setMaxResults($limit);
2153
2154
        if (!$canViewOthers) {
2155
            $q->andWhere('t.created_by = :userId')
2156
                ->setParameter('userId', $this->userHelper->getUser()->getId());
2157
        }
2158
2159
        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
2160
        $chartQuery->applyFilters($q, $filters);
2161
        $chartQuery->applyDateFilters($q, 'date_added');
2162
2163
        return $q->execute()->fetchAll();
2164
    }
2165
2166
    /**
2167
     * Get a list of upcoming emails.
2168
     *
2169
     * @param int  $limit
2170
     * @param bool $canViewOthers
2171
     *
2172
     * @return array
2173
     */
2174
    public function getUpcomingEmails($limit = 10, $canViewOthers = true)
2175
    {
2176
        /** @var \Mautic\CampaignBundle\Entity\LeadEventLogRepository $leadEventLogRepository */
2177
        $leadEventLogRepository = $this->em->getRepository('MauticCampaignBundle:LeadEventLog');
2178
        $leadEventLogRepository->setCurrentUser($this->userHelper->getUser());
2179
2180
        return $leadEventLogRepository->getUpcomingEvents(
2181
            [
2182
                'type'          => 'email.send',
2183
                'limit'         => $limit,
2184
                'canViewOthers' => $canViewOthers,
2185
            ]
2186
        );
2187
    }
2188
2189
    /**
2190
     * @param        $type
2191
     * @param string $filter
2192
     * @param int    $limit
2193
     * @param int    $start
2194
     * @param array  $options
2195
     *
2196
     * @return array
2197
     */
2198
    public function getLookupResults($type, $filter = '', $limit = 10, $start = 0, $options = [])
2199
    {
2200
        $results = [];
2201
        switch ($type) {
2202
            case 'email':
2203
                $emailRepo = $this->getRepository();
2204
                $emailRepo->setCurrentUser($this->userHelper->getUser());
2205
                $emails = $emailRepo->getEmailList(
2206
                    $filter,
2207
                    $limit,
2208
                    $start,
2209
                    $this->security->isGranted('email:emails:viewother'),
2210
                    isset($options['top_level']) ? $options['top_level'] : false,
2211
                    isset($options['email_type']) ? $options['email_type'] : null,
2212
                    isset($options['ignore_ids']) ? $options['ignore_ids'] : [],
2213
                    isset($options['variant_parent']) ? $options['variant_parent'] : null
2214
                );
2215
2216
                foreach ($emails as $email) {
2217
                    $results[$email['language']][$email['id']] = $email['name'];
2218
                }
2219
2220
                //sort by language
2221
                ksort($results);
2222
2223
                break;
2224
        }
2225
2226
        return $results;
2227
    }
2228
2229
    /**
2230
     * @param $sendTo
2231
     */
2232
    private function getContactCompanies(array &$sendTo)
2233
    {
2234
        $fetchCompanies = [];
2235
        foreach ($sendTo as $key => $contact) {
2236
            if (!isset($contact['companies'])) {
2237
                $fetchCompanies[$contact['id']] = $key;
2238
                $sendTo[$key]['companies']      = [];
2239
            }
2240
        }
2241
2242
        if (!empty($fetchCompanies)) {
2243
            // Simple dbal query that fetches lead_id IN $fetchCompanies and returns as array
2244
            $companies = $this->companyModel->getRepository()->getCompaniesForContacts(array_keys($fetchCompanies));
2245
2246
            foreach ($companies as $contactId => $contactCompanies) {
2247
                $key                       = $fetchCompanies[$contactId];
2248
                $sendTo[$key]['companies'] = $contactCompanies;
2249
            }
2250
        }
2251
    }
2252
2253
    /**
2254
     * Send an email to lead(s).
2255
     *
2256
     * @param       $email
2257
     * @param       $users
2258
     * @param mixed $leadFields
2259
     * @param array $tokens
2260
     * @param array $assetAttachments
2261
     * @param bool  $saveStat
2262
     *
2263
     * @return mixed
2264
     *
2265
     * @throws \Doctrine\ORM\ORMException
2266
     */
2267
    public function sendSampleEmailToUser($email, $users, $leadFields = null, $tokens = [], $assetAttachments = [], $saveStat = true)
2268
    {
2269
        if (!$emailId = $email->getId()) {
2270
            return false;
2271
        }
2272
2273
        if (!is_array($users)) {
2274
            $user  = ['id' => $users];
2275
            $users = [$user];
2276
        }
2277
2278
        //get email settings
2279
        $emailSettings = &$this->getEmailSettings($email, false);
2280
2281
        //noone to send to so bail
2282
        if (empty($users)) {
2283
            return false;
2284
        }
2285
2286
        $mailer = $this->mailHelper->getSampleMailer();
2287
        $mailer->setLead($leadFields, true);
2288
        $mailer->setTokens($tokens);
2289
        $mailer->setEmail($email, false, $emailSettings[$emailId]['slots'], $assetAttachments, (!$saveStat));
2290
2291
        $errors = [];
2292
        foreach ($users as $user) {
2293
            $idHash = uniqid();
2294
            $mailer->setIdHash($idHash, $saveStat);
2295
2296
            if (!is_array($user)) {
2297
                $id   = $user;
2298
                $user = ['id' => $id];
2299
            } else {
2300
                $id = $user['id'];
2301
            }
2302
2303
            if (!isset($user['email'])) {
2304
                $userEntity        = $this->userModel->getEntity($id);
2305
                $user['email']     = $userEntity->getEmail();
2306
                $user['firstname'] = $userEntity->getFirstName();
2307
                $user['lastname']  = $userEntity->getLastName();
2308
            }
2309
2310
            if (!$mailer->setTo($user['email'], $user['firstname'].' '.$user['lastname'])) {
2311
                $errors[] = "{$user['email']}: ".$this->translator->trans('mautic.email.bounce.reason.bad_email');
2312
            } else {
2313
                if (!$mailer->queue(true)) {
2314
                    $errorArray = $mailer->getErrors();
2315
                    unset($errorArray['failures']);
2316
                    $errors[] = "{$user['email']}: ".implode('; ', $errorArray);
2317
                }
2318
2319
                if ($saveStat) {
2320
                    $saveEntities[] = $mailer->createEmailStat(false, $user['email']);
2321
                }
2322
            }
2323
        }
2324
2325
        //flush the message
2326
        if (!$mailer->flushQueue()) {
2327
            $errorArray = $mailer->getErrors();
2328
            unset($errorArray['failures']);
2329
            $errors[] = implode('; ', $errorArray);
2330
        }
2331
2332
        if (isset($saveEntities)) {
2333
            $this->getStatRepository()->saveEntities($saveEntities);
2334
        }
2335
2336
        //save some memory
2337
        unset($mailer);
2338
2339
        return $errors;
2340
    }
2341
2342
    /**
2343
     * @param $segmentId
2344
     *
2345
     * @return array
2346
     */
2347
    public function getEmailsIdsWithDependenciesOnSegment($segmentId)
2348
    {
2349
        $entities =  $this->getEntities(
2350
            [
2351
                'filter'         => [
2352
                    'force' => [
2353
                        [
2354
                            'column' => 'l.id',
2355
                            'expr'   => 'eq',
2356
                            'value'  => $segmentId,
2357
                        ],
2358
                    ],
2359
                ],
2360
            ]
2361
        );
2362
2363
        $ids = [];
2364
        foreach ($entities as $entity) {
2365
            $ids[] = $entity->getId();
2366
        }
2367
2368
        return $ids;
2369
    }
2370
}
2371