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')); |
||||
0 ignored issues
–
show
Deprecated Code
introduced
by
Loading history...
|
|||||
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
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) { |
||||
0 ignored issues
–
show
The expression
$campaignId of type integer|null is loosely compared to true ; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
Loading history...
|
|||||
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 |