Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class Copy
|
||||
{
|
||||
/**
|
||||
* MD5 hash of the content.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateCreated;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $body;
|
||||
|
||||
private ?string $bodyText = null;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $subject;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('email_copies')
|
||||
->setCustomRepositoryClass(CopyRepository::class);
|
||||
|
||||
$builder->createField('id', 'string')
|
||||
->makePrimaryKey()
|
||||
->length(32)
|
||||
->build();
|
||||
|
||||
$builder->createField('dateCreated', 'datetime')
|
||||
->columnName('date_created')
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('body', 'text');
|
||||
$builder->addNullableField('bodyText', 'text', 'body_text');
|
||||
|
||||
$builder->addNullableField('subject', 'text');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateCreated()
|
||||
{
|
||||
return $this->dateCreated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateCreated
|
||||
*
|
||||
* @return Copy
|
||||
*/
|
||||
public function setDateCreated($dateCreated)
|
||||
{
|
||||
$this->dateCreated = $dateCreated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody()
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $body
|
||||
*
|
||||
* @return Copy
|
||||
*/
|
||||
public function setBody($body)
|
||||
{
|
||||
$this->body = $body;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSubject()
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $subject
|
||||
*
|
||||
* @return Copy
|
||||
*/
|
||||
public function setSubject($subject)
|
||||
{
|
||||
$this->subject = $subject;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBodyText(): ?string
|
||||
{
|
||||
return $this->bodyText;
|
||||
}
|
||||
|
||||
public function setBodyText(?string $bodyText): self
|
||||
{
|
||||
$this->bodyText = $bodyText;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\NoResultException;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Copy>
|
||||
*/
|
||||
class CopyRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @param string $hash
|
||||
* @param string $subject
|
||||
* @param string $body
|
||||
* @param string $bodyText
|
||||
*/
|
||||
public function saveCopy($hash, $subject, $body, $bodyText): bool
|
||||
{
|
||||
$db = $this->getEntityManager()->getConnection();
|
||||
|
||||
try {
|
||||
$db->insert(
|
||||
MAUTIC_TABLE_PREFIX.'email_copies',
|
||||
[
|
||||
'id' => $hash,
|
||||
'body' => $body,
|
||||
'body_text' => $bodyText,
|
||||
'subject' => $subject,
|
||||
'date_created' => (new \DateTime())->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
error_log($e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string md5 hash or content
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findByHash($string, $subject = null)
|
||||
{
|
||||
if (null !== $subject) {
|
||||
// Combine subject with $string and hash together
|
||||
$string = $subject.$string;
|
||||
}
|
||||
|
||||
// Assume that $string is already a md5 hash if 32 characters
|
||||
$hash = (32 !== strlen($string)) ? $hash = md5($string) : $string;
|
||||
|
||||
$q = $this->createQueryBuilder($this->getTableAlias());
|
||||
$q->where(
|
||||
$q->expr()->eq($this->getTableAlias().'.id', ':id')
|
||||
)
|
||||
->setParameter('id', $hash);
|
||||
|
||||
try {
|
||||
$result = $q->getQuery()->getSingleResult();
|
||||
} catch (NoResultException) {
|
||||
$result = null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'ec';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class EmailDraft
|
||||
{
|
||||
private int $id;
|
||||
|
||||
public function __construct(private Email $email, private ?string $html, private ?string $template, private ?bool $publicPreview = true)
|
||||
{
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('emails_draft')
|
||||
->setCustomRepositoryClass(EmailDraftRepository::class)
|
||||
->addLifecycleEvent('cleanUrlsInContent', Events::preUpdate)
|
||||
->addLifecycleEvent('cleanUrlsInContent', Events::prePersist);
|
||||
|
||||
$builder->addId();
|
||||
$builder->addNullableField('html', Types::TEXT);
|
||||
$builder->addNullableField('template', Types::STRING);
|
||||
$builder->createField('publicPreview', Types::BOOLEAN)
|
||||
->columnName('public_preview')
|
||||
->nullable(false)
|
||||
->option('default', 1)
|
||||
->build();
|
||||
|
||||
$builder->createOneToOne('email', Email::class)
|
||||
->inversedBy('draft')
|
||||
->addJoinColumn('email_id', 'id', false)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle callback to clean URLs in the content.
|
||||
*/
|
||||
public function cleanUrlsInContent(): void
|
||||
{
|
||||
$this->decodeAmpersands($this->html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all links in content and decode &
|
||||
* This even works with double encoded ampersands.
|
||||
*/
|
||||
private function decodeAmpersands(string &$content): void
|
||||
{
|
||||
if (preg_match_all('/((https?|ftps?):\/\/)([a-zA-Z0-9-\.{}]*[a-zA-Z0-9=}]*)(\??)([^\s\"\]]+)?/i', $content, $matches)) {
|
||||
foreach ($matches[0] as $url) {
|
||||
$newUrl = $url;
|
||||
|
||||
while (str_contains($newUrl, '&')) {
|
||||
$newUrl = str_replace('&', '&', $newUrl);
|
||||
}
|
||||
|
||||
$content = str_replace($url, $newUrl, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getEmail(): Email
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getHtml(): string
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
public function setEmail(Email $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function setHtml(string $html): void
|
||||
{
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
public function getTemplate(): string
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
public function setTemplate(string $template): void
|
||||
{
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
public function isPublicPreview(): bool
|
||||
{
|
||||
return $this->publicPreview;
|
||||
}
|
||||
|
||||
public function getPublishStatus(): bool
|
||||
{
|
||||
return $this->publicPreview;
|
||||
}
|
||||
|
||||
public function setPublicPreview(bool $publicPreview): void
|
||||
{
|
||||
$this->publicPreview = $publicPreview;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
class EmailDraftRepository extends CommonRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
|
||||
class EmailReply
|
||||
{
|
||||
private string $id;
|
||||
|
||||
private \DateTimeInterface $dateReplied;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('email_stat_replies')
|
||||
->setCustomRepositoryClass(EmailReplyRepository::class)
|
||||
->addIndex(['stat_id', 'message_id'], 'email_replies')
|
||||
->addIndex(['date_replied'], 'date_email_replied');
|
||||
|
||||
$builder->addUuid();
|
||||
|
||||
$builder->createManyToOne('stat', Stat::class)
|
||||
->inversedBy('replies')
|
||||
->addJoinColumn('stat_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createField('dateReplied', 'datetime')
|
||||
->columnName('date_replied')
|
||||
->build();
|
||||
|
||||
$builder->createField('messageId', 'string')
|
||||
->columnName('message_id')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('emailReply')
|
||||
->addProperties(
|
||||
[
|
||||
'uuid',
|
||||
'dateReplied',
|
||||
'messageId',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $messageId
|
||||
*/
|
||||
public function __construct(
|
||||
private Stat $stat,
|
||||
private $messageId,
|
||||
?\DateTime $dateReplied = null,
|
||||
) {
|
||||
$this->id = Uuid::uuid4()->toString();
|
||||
$this->dateReplied = $dateReplied ?? new \DateTime();
|
||||
}
|
||||
|
||||
public function getId(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function getStat()
|
||||
{
|
||||
return $this->stat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateReplied()
|
||||
{
|
||||
return $this->dateReplied;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMessageId()
|
||||
{
|
||||
return $this->messageId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<EmailReply>
|
||||
*/
|
||||
final class EmailReplyRepository extends CommonRepository implements EmailReplyRepositoryInterface
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* @param int|Lead|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getByLeadIdForTimeline($leadId, $options)
|
||||
{
|
||||
if ($leadId instanceof Lead) {
|
||||
$leadId = $leadId->getId();
|
||||
}
|
||||
$qb = $this->_em->getConnection()->createQueryBuilder();
|
||||
$qb->from(MAUTIC_TABLE_PREFIX.'email_stat_replies', 'reply')
|
||||
->innerJoin('reply', MAUTIC_TABLE_PREFIX.'email_stats', 'stat', 'reply.stat_id = stat.id')
|
||||
->leftJoin('stat', MAUTIC_TABLE_PREFIX.'emails', 'email', 'stat.email_id = email.id')
|
||||
->leftJoin('stat', MAUTIC_TABLE_PREFIX.'email_copies', 'email_copy', 'stat.copy_id = email_copy.id');
|
||||
|
||||
if (null !== $leadId) {
|
||||
$qb->andWhere('stat.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (!empty($options['fromDate'])) {
|
||||
/** @var \DateTime $fromDate */
|
||||
$fromDate = $options['fromDate'];
|
||||
$qb->andWhere('reply.date_replied >= :fromDate')
|
||||
->setParameter('fromDate', $fromDate->format('Y-m-d H:i:s'));
|
||||
}
|
||||
if (!empty($options['toDate'])) {
|
||||
/** @var \DateTime $toDate */
|
||||
$toDate = $options['toDate'];
|
||||
$qb->andWhere('reply.date_replied <= :toDate')
|
||||
->setParameter('toDate', $toDate->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
$qb->addSelect('reply.id')
|
||||
->addSelect('reply.date_replied')
|
||||
->addSelect('stat.lead_id')
|
||||
->addSelect('email.name AS email_name')
|
||||
->addSelect('email.subject')
|
||||
->addSelect('email_copy.subject AS storedSubject');
|
||||
|
||||
return $this->getTimelineResults(
|
||||
$qb,
|
||||
$options,
|
||||
'storedSubject, email.subject',
|
||||
'reply.id',
|
||||
[],
|
||||
['date_replied']
|
||||
);
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'reply';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
/**
|
||||
* Interface EmailReplyRepositoryInterface.
|
||||
*/
|
||||
interface EmailReplyRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param int|Lead $leadId
|
||||
* @param array $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getByLeadIdForTimeline($leadId, $options);
|
||||
}
|
||||
@@ -0,0 +1,846 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\ChannelBundle\Entity\MessageQueue;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Entity\DoNotContact;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Email>
|
||||
*/
|
||||
class EmailRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
public const EMAILS_PREFIX = 'e';
|
||||
|
||||
public const DNC_PREFIX = 'dnc';
|
||||
|
||||
public const TRACKABLE_PREFIX = 'tr';
|
||||
|
||||
public const REDIRECT_PREFIX = 'pr';
|
||||
|
||||
/**
|
||||
* Get an array of do not email.
|
||||
*
|
||||
* @param array $leadIds
|
||||
*/
|
||||
public function getDoNotEmailList($leadIds = []): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('l.id, l.email')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
|
||||
->leftJoin('dnc', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = dnc.lead_id')
|
||||
->where($q->expr()->eq('dnc.channel', $q->expr()->literal('email')))
|
||||
->andWhere($q->expr()->neq('l.email', $q->expr()->literal('')));
|
||||
|
||||
if ($leadIds) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('l.id', $leadIds)
|
||||
);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$dnc = [];
|
||||
foreach ($results as $r) {
|
||||
$dnc[$r['id']] = strtolower($r['email']);
|
||||
}
|
||||
|
||||
return $dnc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if an email is set as do not contact.
|
||||
*
|
||||
* @param string $email
|
||||
*
|
||||
* @return false|array{id: numeric-string, unsubscribed: bool, bounced: bool, manual: bool, comments: string}
|
||||
*/
|
||||
public function checkDoNotEmail($email)
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('dnc.*')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
|
||||
->leftJoin('dnc', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = dnc.lead_id')
|
||||
->where($q->expr()->eq('dnc.channel', $q->expr()->literal('email')))
|
||||
->andWhere('l.email = :email')
|
||||
->setParameter('email', $email);
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$dnc = count($results) ? $results[0] : null;
|
||||
|
||||
if (null === $dnc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dnc['reason'] = (int) $dnc['reason'];
|
||||
|
||||
return [
|
||||
'id' => $dnc['id'],
|
||||
'unsubscribed' => (DoNotContact::UNSUBSCRIBED === $dnc['reason']),
|
||||
'bounced' => (DoNotContact::BOUNCED === $dnc['reason']),
|
||||
'manual' => (DoNotContact::MANUAL === $dnc['reason']),
|
||||
'comments' => $dnc['comments'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete DNC row.
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function deleteDoNotEmailEntry($id): void
|
||||
{
|
||||
$this->getEntityManager()->getConnection()->delete(MAUTIC_TABLE_PREFIX.'lead_donotcontact', ['id' => (int) $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of entities.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this->getEntityManager()
|
||||
->createQueryBuilder()
|
||||
->select('e')
|
||||
->from(Email::class, 'e', 'e.id');
|
||||
if (empty($args['iterable_mode'])) {
|
||||
$q->leftJoin('e.category', 'c');
|
||||
|
||||
if (empty($args['ignoreListJoin']) && (!isset($args['email_type']) || 'list' == $args['email_type'])) {
|
||||
$q->leftJoin('e.lists', 'l');
|
||||
}
|
||||
}
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get amounts of sent and read emails.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSentReadCount()
|
||||
{
|
||||
// Get entities
|
||||
$q = $this->getEntityManager()->createQueryBuilder();
|
||||
$q->select('SUM(e.sentCount) as sent_count, SUM(e.readCount) as read_count')
|
||||
->from(Email::class, 'e');
|
||||
$results = $q->getQuery()->getSingleResult(Query::HYDRATE_ARRAY);
|
||||
|
||||
if (!isset($results['sent_count'])) {
|
||||
$results['sent_count'] = 0;
|
||||
}
|
||||
if (!isset($results['read_count'])) {
|
||||
$results['read_count'] = 0;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $emailId
|
||||
* @param int[]|null $variantIds
|
||||
* @param int[]|null $listIds
|
||||
* @param bool $countOnly
|
||||
* @param int|null $limit
|
||||
* @param int|null $minContactId
|
||||
* @param int|null $maxContactId
|
||||
* @param bool $countWithMaxMin
|
||||
* @param \DateTime|null $maxDate
|
||||
*
|
||||
* @return QueryBuilder|int|array
|
||||
*/
|
||||
public function getEmailPendingQuery(
|
||||
$emailId,
|
||||
$variantIds = null,
|
||||
$listIds = null,
|
||||
$countOnly = false,
|
||||
$limit = null,
|
||||
$minContactId = null,
|
||||
$maxContactId = null,
|
||||
$countWithMaxMin = false,
|
||||
$maxDate = null,
|
||||
?int $maxThreads = null,
|
||||
?int $threadId = null,
|
||||
?\DateTimeInterface $sendStopDate = null,
|
||||
) {
|
||||
// Do not include leads in the do not contact table
|
||||
$dncQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$dncQb->select('dnc.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_donotcontact', 'dnc')
|
||||
->where(
|
||||
$dncQb->expr()->and(
|
||||
$dncQb->expr()->eq('dnc.lead_id', 'l.id'),
|
||||
$dncQb->expr()->eq('dnc.channel', $dncQb->expr()->literal('email'))
|
||||
));
|
||||
|
||||
// Do not include contacts where the message is pending in the message queue
|
||||
$mqQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$mqQb->select('mq.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'message_queue', 'mq')
|
||||
->where(
|
||||
$mqQb->expr()->and(
|
||||
$mqQb->expr()->eq('mq.lead_id', 'l.id'),
|
||||
$mqQb->expr()->neq('mq.status', $mqQb->expr()->literal(MessageQueue::STATUS_SENT)),
|
||||
$mqQb->expr()->eq('mq.channel', $mqQb->expr()->literal('email'))
|
||||
)
|
||||
);
|
||||
|
||||
// Do not include leads that have already been emailed
|
||||
$statQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$statQb->select('stat.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'stat');
|
||||
|
||||
$statQb->andWhere($statQb->expr()->isNotNull('stat.lead_id'));
|
||||
|
||||
if ($variantIds) {
|
||||
if (!in_array($emailId, $variantIds)) {
|
||||
$variantIds[] = (string) $emailId;
|
||||
}
|
||||
$statQb->andWhere($statQb->expr()->in('stat.email_id', $variantIds));
|
||||
$mqQb->andWhere($mqQb->expr()->in('mq.channel_id', $variantIds));
|
||||
} else {
|
||||
$statQb->andWhere($statQb->expr()->eq('stat.email_id', (int) $emailId));
|
||||
$mqQb->andWhere($mqQb->expr()->eq('mq.channel_id', (int) $emailId));
|
||||
}
|
||||
|
||||
// Only include those who belong to the associated lead lists
|
||||
if (is_null($listIds)) {
|
||||
// Get a list of lists associated with this email
|
||||
$lists = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('el.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_list_xref', 'el')
|
||||
->where('el.email_id = '.(int) $emailId)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$listIds = array_column($lists, 'leadlist_id');
|
||||
|
||||
if (empty($listIds)) {
|
||||
// Prevent fatal error
|
||||
return ($countOnly) ? 0 : [];
|
||||
}
|
||||
} elseif (!is_array($listIds)) {
|
||||
$listIds = [$listIds];
|
||||
}
|
||||
|
||||
// Main query
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
// Only include those in associated segments
|
||||
$segmentQb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$segmentQb->select('ll.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
|
||||
->where(
|
||||
$segmentQb->expr()->and(
|
||||
$segmentQb->expr()->eq('ll.lead_id', 'l.id'),
|
||||
$segmentQb->expr()->in('ll.leadlist_id', $listIds),
|
||||
$segmentQb->expr()->eq('ll.manually_removed', ':false')
|
||||
)
|
||||
);
|
||||
|
||||
if (null !== $maxDate) {
|
||||
$segmentQb->andWhere($segmentQb->expr()->lte('ll.date_added', ':max_date'));
|
||||
$segmentQb->setParameter('max_date', $maxDate, \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE);
|
||||
}
|
||||
|
||||
if ($sendStopDate) {
|
||||
$segmentQb->andWhere($segmentQb->expr()->lt('ll.date_added', ':sendStopDate'));
|
||||
$q->setParameter('sendStopDate', (new DateTimeHelper($sendStopDate))->toUtcString());
|
||||
}
|
||||
|
||||
if ($countOnly) {
|
||||
$q->select('count(*) as count');
|
||||
if ($countWithMaxMin) {
|
||||
$q->addSelect('MIN(l.id) as min_id, MAX(l.id) as max_id');
|
||||
}
|
||||
} else {
|
||||
$q->select('l.*');
|
||||
}
|
||||
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->andWhere(sprintf('l.id IN (%s)', $segmentQb->getSQL()))
|
||||
->andWhere(sprintf('l.id NOT IN (%s)', $dncQb->getSQL()))
|
||||
->andWhere(sprintf('l.id NOT IN (%s)', $statQb->getSQL()))
|
||||
->andWhere(sprintf('l.id NOT IN (%s)', $mqQb->getSQL()))
|
||||
->setParameter('false', false, 'boolean');
|
||||
|
||||
$excludedListQb = $this->getExcludedListQuery((int) $emailId);
|
||||
|
||||
if ($excludedListQb) {
|
||||
$q->andWhere(sprintf('l.id NOT IN (%s)', $excludedListQb->getSQL()));
|
||||
}
|
||||
|
||||
// Do not include leads which are not subscribed to the category set for email.
|
||||
$unsubscribeLeadsQb = $this->getCategoryUnsubscribedLeadsQuery((int) $emailId);
|
||||
$q->andWhere(sprintf('l.id NOT IN (%s)', $unsubscribeLeadsQb->getSQL()));
|
||||
|
||||
$q = $this->setMinMaxIds($q, 'l.id', $minContactId, $maxContactId);
|
||||
|
||||
// Has an email
|
||||
$q->andWhere(
|
||||
$q->expr()->and(
|
||||
$q->expr()->isNotNull('l.email'),
|
||||
$q->expr()->neq('l.email', $q->expr()->literal(''))
|
||||
)
|
||||
);
|
||||
|
||||
if ($threadId && $maxThreads) {
|
||||
if ($threadId <= $maxThreads) {
|
||||
$q->andWhere('MOD((l.id + :threadShift), :maxThreads) = 0')
|
||||
->setParameter('threadShift', $threadId - 1, \Doctrine\DBAL\ParameterType::INTEGER)
|
||||
->setParameter('maxThreads', $maxThreads, \Doctrine\DBAL\ParameterType::INTEGER);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult(0)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $emailId
|
||||
* @param int[]|null $variantIds
|
||||
* @param int[]|null $listIds
|
||||
* @param bool $countOnly
|
||||
* @param int|null $limit
|
||||
* @param int|null $minContactId
|
||||
* @param int|null $maxContactId
|
||||
* @param bool $countWithMaxMin
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getEmailPendingLeads(
|
||||
$emailId,
|
||||
$variantIds = null,
|
||||
$listIds = null,
|
||||
$countOnly = false,
|
||||
$limit = null,
|
||||
$minContactId = null,
|
||||
$maxContactId = null,
|
||||
$countWithMaxMin = false,
|
||||
?int $maxThreads = null,
|
||||
?int $threadId = null,
|
||||
?\DateTimeInterface $sendStopDate = null,
|
||||
) {
|
||||
$q = $this->getEmailPendingQuery(
|
||||
$emailId,
|
||||
$variantIds,
|
||||
$listIds,
|
||||
$countOnly,
|
||||
$limit,
|
||||
$minContactId,
|
||||
$maxContactId,
|
||||
$countWithMaxMin,
|
||||
null,
|
||||
$maxThreads,
|
||||
$threadId,
|
||||
$sendStopDate
|
||||
);
|
||||
|
||||
if (!($q instanceof QueryBuilder)) {
|
||||
return $q;
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
if ($countOnly && $countWithMaxMin) {
|
||||
// returns array in format ['count' => #, ['min_id' => #, 'max_id' => #]]
|
||||
return $results[0];
|
||||
} elseif ($countOnly) {
|
||||
return (isset($results[0])) ? $results[0]['count'] : 0;
|
||||
} else {
|
||||
$leads = [];
|
||||
foreach ($results as $r) {
|
||||
$leads[$r['id']] = $r;
|
||||
}
|
||||
|
||||
return $leads;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array<int|string> $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param bool $viewOther
|
||||
* @param bool $topLevel
|
||||
* @param string|null $emailType
|
||||
* @param int|null $variantParentId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEmailList($search = '', $limit = 10, $start = 0, $viewOther = false, $topLevel = false, $emailType = null, array $ignoreIds = [], $variantParentId = null)
|
||||
{
|
||||
$q = $this->createQueryBuilder('e');
|
||||
$q->select('partial e.{id, subject, name, language}');
|
||||
|
||||
if (!empty($search)) {
|
||||
if (is_array($search)) {
|
||||
$search = array_map('intval', $search);
|
||||
$q->andWhere($q->expr()->in('e.id', ':search'))
|
||||
->setParameter('search', $search);
|
||||
} else {
|
||||
$q->andWhere($q->expr()->like('e.name', ':search'))
|
||||
->setParameter('search', "%{$search}%");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$viewOther) {
|
||||
$q->andWhere($q->expr()->eq('e.createdBy', ':id'))
|
||||
->setParameter('id', $this->currentUser->getId());
|
||||
}
|
||||
|
||||
if ($topLevel) {
|
||||
if (true === $topLevel || 'variant' == $topLevel) {
|
||||
$q->andWhere($q->expr()->isNull('e.variantParent'));
|
||||
} elseif ('translation' == $topLevel) {
|
||||
$q->andWhere($q->expr()->isNull('e.translationParent'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($variantParentId) {
|
||||
$q->andWhere(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('IDENTITY(e.variantParent)', (int) $variantParentId),
|
||||
$q->expr()->eq('e.id', (int) $variantParentId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($ignoreIds)) {
|
||||
$q->andWhere($q->expr()->notIn('e.id', ':emailIds'))
|
||||
->setParameter('emailIds', $ignoreIds);
|
||||
}
|
||||
|
||||
if (!empty($emailType)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('e.emailType', $q->expr()->literal($emailType))
|
||||
);
|
||||
}
|
||||
|
||||
$q->orderBy('e.name');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
public function getSentReadNotReadCount(QueryBuilder $queryBuilder): array
|
||||
{
|
||||
$queryBuilder->resetQueryPart('groupBy');
|
||||
$queryBuilder->resetQueryParts(['join']);
|
||||
|
||||
$queryBuilder->select('SUM( e.sent_count) as sent_count, SUM( e.read_count) as read_count');
|
||||
$results = $queryBuilder->executeQuery()->fetchAssociative();
|
||||
|
||||
if ($results) {
|
||||
$results['sent_count'] = (int) $results['sent_count'];
|
||||
$results['read_count'] = (int) $results['read_count'];
|
||||
$results['not_read'] = $results['sent_count'] - $results['read_count'];
|
||||
} else {
|
||||
$results = [];
|
||||
$results['not_read'] = $results['sent_count'] = $results['read_count'] = 0;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getUnsubscribedCount(QueryBuilder $queryBuilder): int
|
||||
{
|
||||
$queryBuilder->resetQueryParts(['join']);
|
||||
$this->addDNCTableForEmails($queryBuilder);
|
||||
$queryBuilder->select('e.id as email_id, dnc.lead_id');
|
||||
$queryBuilder->andWhere('dnc.reason='.DoNotContact::UNSUBSCRIBED);
|
||||
|
||||
return $queryBuilder->executeQuery()->rowCount();
|
||||
}
|
||||
|
||||
public function getUniqueClicks(QueryBuilder $queryBuilder): int
|
||||
{
|
||||
$this->addTrackableTablesForEmailStats($queryBuilder);
|
||||
$queryBuilder->select('SUM( tr.unique_hits) as `unique_clicks`');
|
||||
|
||||
return (int) $queryBuilder->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
private function addTrackableTablesForEmailStats(QueryBuilder $qb): void
|
||||
{
|
||||
$trTable = MAUTIC_TABLE_PREFIX.'channel_url_trackables';
|
||||
$prTable = MAUTIC_TABLE_PREFIX.'page_redirects';
|
||||
|
||||
if (!$this->isJoined($qb, $trTable, self::EMAILS_PREFIX, self::TRACKABLE_PREFIX)) {
|
||||
$qb->leftJoin(
|
||||
self::EMAILS_PREFIX,
|
||||
$trTable,
|
||||
self::TRACKABLE_PREFIX,
|
||||
'e.id = tr.channel_id AND tr.channel = \'email\''
|
||||
);
|
||||
}
|
||||
if (!$this->isJoined($qb, $prTable, self::TRACKABLE_PREFIX, self::REDIRECT_PREFIX)) {
|
||||
$qb->leftJoin(
|
||||
self::TRACKABLE_PREFIX,
|
||||
$prTable,
|
||||
self::REDIRECT_PREFIX,
|
||||
'tr.redirect_id = pr.id'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Do Not Contact table to the query builder.
|
||||
*/
|
||||
private function addDNCTableForEmails(QueryBuilder $qb): void
|
||||
{
|
||||
$table = MAUTIC_TABLE_PREFIX.'lead_donotcontact';
|
||||
|
||||
if (!$this->isJoined($qb, $table, self::EMAILS_PREFIX, self::DNC_PREFIX)) {
|
||||
$qb->leftJoin(
|
||||
self::EMAILS_PREFIX,
|
||||
$table,
|
||||
self::DNC_PREFIX,
|
||||
'e.id = dnc.channel_id AND dnc.channel=\'email\''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function isJoined(QueryBuilder $query, string $table, string $fromAlias, string $alias): bool
|
||||
{
|
||||
$joins = $query->getQueryParts()['join'][$fromAlias] ?? null;
|
||||
|
||||
if (empty($joins)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($joins[$fromAlias] as $join) {
|
||||
if ($join['joinTable'] == $table && $join['joinAlias'] == $alias) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
|
||||
* @param object $filter
|
||||
*/
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause($q, $filter, [
|
||||
'e.name',
|
||||
'e.subject',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Doctrine\ORM\QueryBuilder|QueryBuilder $q
|
||||
* @param object $filter
|
||||
*/
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.email.email.searchcommand.isexpired'):
|
||||
case $this->translator->trans('mautic.email.email.searchcommand.isexpired', [], null, 'en_US'):
|
||||
$expr = sprintf(
|
||||
"(e.isPublished = :%1\$s AND e.publishDown IS NOT NULL AND e.publishDown <> '' AND e.publishDown < CURRENT_TIMESTAMP())",
|
||||
$unique
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.email.email.searchcommand.ispending'):
|
||||
case $this->translator->trans('mautic.email.email.searchcommand.ispending', [], null, 'en_US'):
|
||||
$expr = sprintf(
|
||||
"(e.isPublished = :%1\$s AND e.publishUp IS NOT NULL AND e.publishUp <> '' AND e.publishUp > CURRENT_TIMESTAMP())",
|
||||
$unique
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.lang'):
|
||||
$langUnique = $this->generateRandomParameterName();
|
||||
$langValue = $filter->string.'_%';
|
||||
$forceParameters = [
|
||||
$langUnique => $langValue,
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = '('.$q->expr()->eq('e.language', ":$unique").' OR '.$q->expr()->like('e.language', ":$langUnique").')';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'email_id',
|
||||
'email_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.isuncategorized',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.email.email.searchcommand.isexpired',
|
||||
'mautic.email.email.searchcommand.ispending',
|
||||
'mautic.core.searchcommand.category',
|
||||
'mautic.core.searchcommand.lang',
|
||||
'mautic.project.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string>>
|
||||
*/
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['e.name', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'e';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets variant_start_date, variant_read_count, variant_sent_count.
|
||||
*
|
||||
* @param string[]|string|int $relatedIds
|
||||
* @param string $date
|
||||
*/
|
||||
public function resetVariants($relatedIds, $date): void
|
||||
{
|
||||
if (!is_array($relatedIds)) {
|
||||
$relatedIds = [(string) $relatedIds];
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$qb->update(MAUTIC_TABLE_PREFIX.'emails')
|
||||
->set('variant_read_count', 0)
|
||||
->set('variant_sent_count', 0)
|
||||
->set('variant_start_date', ':date')
|
||||
->setParameter('date', $date)
|
||||
->where(
|
||||
$qb->expr()->in('id', $relatedIds)
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function upCountSent(int $id, int $increaseBy = 1, bool $variant = false): void
|
||||
{
|
||||
if ($increaseBy <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$connection = $this->getEntityManager()->getConnection();
|
||||
$updateQuery = $connection->createQueryBuilder()
|
||||
->update(MAUTIC_TABLE_PREFIX.'emails')
|
||||
->set('sent_count', 'sent_count + :increaseBy')
|
||||
->where('id = :id');
|
||||
|
||||
if ($variant) {
|
||||
$updateQuery->set('variant_sent_count', 'variant_sent_count + :increaseBy');
|
||||
}
|
||||
|
||||
$updateQuery
|
||||
->setParameter('increaseBy', $increaseBy)
|
||||
->setParameter('id', $id);
|
||||
|
||||
// Try to execute 3 times before throwing the exception
|
||||
$retrialLimit = 3;
|
||||
while ($retrialLimit >= 0) {
|
||||
try {
|
||||
$updateQuery->executeStatement();
|
||||
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
--$retrialLimit;
|
||||
if (0 === $retrialLimit) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function incrementRead(int $emailId, string $statId, bool $isVariant = false): void
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$subQuery = $this->getEntityManager()->getConnection()->createQueryBuilder()
|
||||
->select('es.email_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'es')
|
||||
->where('es.id = :statId')
|
||||
->andWhere('es.is_read = 1');
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'emails', 'e')
|
||||
->set('read_count', 'read_count + 1')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('e.id', ':emailId'),
|
||||
$q->expr()->notIn('e.id', $subQuery->getSQL())
|
||||
)
|
||||
)
|
||||
->setParameter('emailId', $emailId)
|
||||
->setParameter('statId', $statId);
|
||||
|
||||
if ($isVariant) {
|
||||
$q->set('variant_read_count', 'variant_read_count + 1');
|
||||
}
|
||||
|
||||
// Try to execute 3 times before throwing the exception
|
||||
$retrialLimit = 3;
|
||||
while ($retrialLimit >= 0) {
|
||||
try {
|
||||
$q->executeStatement();
|
||||
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
--$retrialLimit;
|
||||
if (0 === $retrialLimit) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<Email>
|
||||
*/
|
||||
public function getPublishedBroadcastsIterable(?int $id = null): iterable
|
||||
{
|
||||
return $this->getPublishedBroadcastsQuery($id)->toIterable();
|
||||
}
|
||||
|
||||
private function getPublishedBroadcastsQuery(?int $id = null): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder($this->getTableAlias());
|
||||
$expr = $this->getPublishedByDateExpression($qb, null, true, true, false);
|
||||
|
||||
$expr->add(
|
||||
$qb->expr()->eq($this->getTableAlias().'.emailType', $qb->expr()->literal('list'))
|
||||
);
|
||||
|
||||
if (null !== $id && 0 !== $id) {
|
||||
$expr->add(
|
||||
$qb->expr()->eq($this->getTableAlias().'.id', (int) $id)
|
||||
);
|
||||
}
|
||||
$qb->where($expr);
|
||||
|
||||
return $qb->getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Max and/or Min ID where conditions to the query builder.
|
||||
*
|
||||
* @param string $column
|
||||
* @param int $minContactId
|
||||
* @param int $maxContactId
|
||||
*/
|
||||
private function setMinMaxIds(QueryBuilder $q, $column, $minContactId, $maxContactId): QueryBuilder
|
||||
{
|
||||
if ($minContactId && is_numeric($minContactId)) {
|
||||
$q->andWhere($column.' >= :minContactId');
|
||||
$q->setParameter('minContactId', $minContactId);
|
||||
}
|
||||
|
||||
if ($maxContactId && is_numeric($maxContactId)) {
|
||||
$q->andWhere($column.' <= :maxContactId');
|
||||
$q->setParameter('maxContactId', $maxContactId);
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
private function getCategoryUnsubscribedLeadsQuery(int $emailId): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()
|
||||
->createQueryBuilder();
|
||||
|
||||
return $qb->select('lc.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_categories', 'lc')
|
||||
->innerJoin('lc', MAUTIC_TABLE_PREFIX.'emails', 'e', 'e.category_id = lc.category_id')
|
||||
->where($qb->expr()->eq('e.id', $emailId))
|
||||
->andWhere('lc.manually_removed = 1');
|
||||
}
|
||||
|
||||
private function getExcludedListQuery(int $emailId): ?QueryBuilder
|
||||
{
|
||||
$connection = $this->getEntityManager()
|
||||
->getConnection();
|
||||
$excludedListIds = $connection->createQueryBuilder()
|
||||
->select('eel.leadlist_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_list_excluded', 'eel')
|
||||
->where('eel.email_id = :emailId')
|
||||
->setParameter('emailId', $emailId)
|
||||
->executeQuery()
|
||||
->fetchFirstColumn();
|
||||
|
||||
if (!$excludedListIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$queryBuilder->select('ll.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'll')
|
||||
->where($queryBuilder->expr()->in('ll.leadlist_id', $excludedListIds));
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadList;
|
||||
|
||||
class Stat
|
||||
{
|
||||
/**
|
||||
* @var int Limit number of stored 'openDetails'
|
||||
*/
|
||||
public const MAX_OPEN_DETAILS = 1000;
|
||||
|
||||
public const TABLE_NAME = 'email_stats';
|
||||
|
||||
private ?string $id = null;
|
||||
|
||||
/**
|
||||
* @var Email|null
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $emailAddress;
|
||||
|
||||
/**
|
||||
* @var LeadList|null
|
||||
*/
|
||||
private $list;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
private ?\DateTimeInterface $dateSent = null;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isRead = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isFailed = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $viewedInBrowser = false;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $dateRead;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $trackingHash;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $retryCount = 0;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $sourceId;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $tokens = [];
|
||||
|
||||
/**
|
||||
* @var Copy|null
|
||||
*/
|
||||
private $storedCopy;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $openCount = 0;
|
||||
|
||||
private ?\DateTimeInterface $lastOpened = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $openDetails = [];
|
||||
|
||||
/**
|
||||
* @var ArrayCollection|EmailReply[]
|
||||
*/
|
||||
private $replies;
|
||||
|
||||
/**
|
||||
* @var array<string,mixed[]>
|
||||
*/
|
||||
private $changes = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->replies = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(StatRepository::class)
|
||||
->addIndex(['email_id', 'lead_id'], 'stat_email_search')
|
||||
->addIndex(['lead_id', 'email_id'], 'stat_email_search2')
|
||||
->addIndex(['is_failed'], 'stat_email_failed_search')
|
||||
->addIndex(['is_read', 'date_sent'], 'is_read_date_sent')
|
||||
->addIndex(['tracking_hash'], 'stat_email_hash_search')
|
||||
->addIndex(['source', 'source_id'], 'stat_email_source_search')
|
||||
->addIndex(['date_sent'], 'email_date_sent')
|
||||
->addIndex(['date_read', 'lead_id'], 'email_date_read_lead')
|
||||
->addIndex(['lead_id', 'date_sent'], 'stat_email_lead_id_date_sent')
|
||||
->addIndex(['email_id', 'is_read'], 'stat_email_email_id_is_read');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createManyToOne('email', 'Email')
|
||||
->inversedBy('stats')
|
||||
->addJoinColumn('email_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addLead(true, 'SET NULL');
|
||||
|
||||
$builder->createField('emailAddress', 'string')
|
||||
->columnName('email_address')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('list', LeadList::class)
|
||||
->addJoinColumn('list_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('dateSent', 'datetime')
|
||||
->columnName('date_sent')
|
||||
->build();
|
||||
|
||||
$builder->createField('isRead', 'boolean')
|
||||
->columnName('is_read')
|
||||
->build();
|
||||
|
||||
$builder->createField('isFailed', 'boolean')
|
||||
->columnName('is_failed')
|
||||
->build();
|
||||
|
||||
$builder->createField('viewedInBrowser', 'boolean')
|
||||
->columnName('viewed_in_browser')
|
||||
->build();
|
||||
|
||||
$builder->createField('dateRead', 'datetime')
|
||||
->columnName('date_read')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('trackingHash', 'string')
|
||||
->columnName('tracking_hash')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('retryCount', 'integer')
|
||||
->columnName('retry_count')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('source', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('sourceId', 'integer')
|
||||
->columnName('source_id')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('tokens', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('storedCopy', Copy::class)
|
||||
->addJoinColumn('copy_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('openCount', 'integer', 'open_count');
|
||||
|
||||
$builder->addNullableField('lastOpened', 'datetime', 'last_opened');
|
||||
|
||||
$builder->addNullableField('openDetails', 'array', 'open_details');
|
||||
|
||||
$builder->createOneToMany('replies', EmailReply::class)
|
||||
->mappedBy('stat')
|
||||
->fetchExtraLazy()
|
||||
->cascadeAll()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('stat')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'emailAddress',
|
||||
'ipAddress',
|
||||
'dateSent',
|
||||
'isRead',
|
||||
'isFailed',
|
||||
'dateRead',
|
||||
'retryCount',
|
||||
'source',
|
||||
'openCount',
|
||||
'lastOpened',
|
||||
'sourceId',
|
||||
'trackingHash',
|
||||
'viewedInBrowser',
|
||||
'lead',
|
||||
'email',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getDateRead(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dateRead;
|
||||
}
|
||||
|
||||
public function setDateRead(?\DateTimeInterface $dateRead): void
|
||||
{
|
||||
$dateRead = $this->toDateTime($dateRead);
|
||||
$this->addChange('dateRead', $this->dateRead, $dateRead);
|
||||
$this->dateRead = $dateRead;
|
||||
}
|
||||
|
||||
public function getDateSent(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->dateSent;
|
||||
}
|
||||
|
||||
public function setDateSent(?\DateTimeInterface $dateSent): void
|
||||
{
|
||||
$dateSent = $this->toDateTime($dateSent);
|
||||
$this->addChange('dateSent', $this->dateSent, $dateSent);
|
||||
$this->dateSent = $dateSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Email|null
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(?Email $email = null): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress|null
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param IpAddress|null $ip
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ip): void
|
||||
{
|
||||
$this->ipAddress = $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsRead()
|
||||
{
|
||||
return $this->isRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isRead()
|
||||
{
|
||||
return $this->getIsRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isRead
|
||||
*/
|
||||
public function setIsRead($isRead): void
|
||||
{
|
||||
$this->addChange('isRead', $this->isRead, $isRead);
|
||||
$this->isRead = $isRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead|null
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
public function setLead(?Lead $lead = null): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTrackingHash()
|
||||
{
|
||||
return $this->trackingHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $trackingHash
|
||||
*/
|
||||
public function setTrackingHash($trackingHash): void
|
||||
{
|
||||
$this->trackingHash = $trackingHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadList|null
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param LeadList|null $list
|
||||
*/
|
||||
public function setList($list): void
|
||||
{
|
||||
$this->list = $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getRetryCount()
|
||||
{
|
||||
return $this->retryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $retryCount
|
||||
*/
|
||||
public function setRetryCount($retryCount): void
|
||||
{
|
||||
$this->addChange('retryCount', $this->retryCount, $retryCount);
|
||||
$this->retryCount = $retryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the retry count.
|
||||
*/
|
||||
public function upRetryCount(): void
|
||||
{
|
||||
$this->addChange('retryCount', $this->retryCount, $this->retryCount + 1);
|
||||
++$this->retryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsFailed()
|
||||
{
|
||||
return $this->isFailed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isFailed
|
||||
*/
|
||||
public function setIsFailed($isFailed): void
|
||||
{
|
||||
$this->addChange('isFailed', $this->isFailed, $isFailed);
|
||||
$this->isFailed = $isFailed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isFailed()
|
||||
{
|
||||
return $this->getIsFailed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getEmailAddress()
|
||||
{
|
||||
return $this->emailAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $emailAddress
|
||||
*/
|
||||
public function setEmailAddress($emailAddress): void
|
||||
{
|
||||
$this->addChange('emailAddress', $this->emailAddress, $emailAddress);
|
||||
$this->emailAddress = $emailAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getViewedInBrowser()
|
||||
{
|
||||
return $this->viewedInBrowser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $viewedInBrowser
|
||||
*/
|
||||
public function setViewedInBrowser($viewedInBrowser): void
|
||||
{
|
||||
$this->addChange('viewedInBrowser', $this->viewedInBrowser, $viewedInBrowser);
|
||||
$this->viewedInBrowser = $viewedInBrowser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $source
|
||||
*/
|
||||
public function setSource($source): void
|
||||
{
|
||||
$this->addChange('source', $this->source, $source);
|
||||
$this->source = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getSourceId()
|
||||
{
|
||||
return $this->sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $sourceId
|
||||
*/
|
||||
public function setSourceId($sourceId): void
|
||||
{
|
||||
$this->addChange('sourceId', $this->sourceId, (int) $sourceId);
|
||||
$this->sourceId = (int) $sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTokens()
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
public function setTokens(array $tokens): void
|
||||
{
|
||||
$this->tokens = $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getOpenCount()
|
||||
{
|
||||
return $this->openCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $openCount
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function setOpenCount($openCount)
|
||||
{
|
||||
$this->addChange('openCount', $this->openCount, $openCount);
|
||||
$this->openCount = $openCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $details
|
||||
*/
|
||||
public function addOpenDetails($details): void
|
||||
{
|
||||
if (self::MAX_OPEN_DETAILS > $this->getOpenCount()) {
|
||||
$this->openDetails[] = $details;
|
||||
}
|
||||
|
||||
++$this->openCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Up the sent count.
|
||||
*
|
||||
* @return Stat
|
||||
*/
|
||||
public function upOpenCount()
|
||||
{
|
||||
$count = (int) $this->openCount + 1;
|
||||
$this->addChange('openCount', $this->openCount, $count);
|
||||
$this->openCount = $count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastOpened(): ?\DateTimeInterface
|
||||
{
|
||||
return $this->lastOpened;
|
||||
}
|
||||
|
||||
public function setLastOpened(?\DateTimeInterface $lastOpened): self
|
||||
{
|
||||
$lastOpened = $this->toDateTime($lastOpened);
|
||||
$this->addChange('lastOpened', $this->lastOpened, $lastOpened);
|
||||
$this->lastOpened = $lastOpened;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getOpenDetails()
|
||||
{
|
||||
return $this->openDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function setOpenDetails(array $openDetails)
|
||||
{
|
||||
$this->openDetails = $openDetails;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Copy|null
|
||||
*/
|
||||
public function getStoredCopy()
|
||||
{
|
||||
return $this->storedCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Stat
|
||||
*/
|
||||
public function setStoredCopy(Copy $storedCopy)
|
||||
{
|
||||
$this->storedCopy = $storedCopy;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection|EmailReply[]
|
||||
*/
|
||||
public function getReplies()
|
||||
{
|
||||
return $this->replies;
|
||||
}
|
||||
|
||||
public function addReply(EmailReply $reply): void
|
||||
{
|
||||
$this->addChange('replyAdded', false, true);
|
||||
$this->replies[] = $reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed[]>
|
||||
*/
|
||||
public function getChanges(): array
|
||||
{
|
||||
return $this->changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $currentValue
|
||||
* @param mixed $newValue
|
||||
*/
|
||||
private function addChange(string $property, $currentValue, $newValue): void
|
||||
{
|
||||
if ($currentValue === $newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->changes[$property] = [$currentValue, $newValue];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|\DateTimeImmutable|null $dateTime
|
||||
*/
|
||||
private function toDateTime($dateTime): ?\DateTime
|
||||
{
|
||||
return $dateTime instanceof \DateTimeImmutable ? \DateTime::createFromImmutable($dateTime) : $dateTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\LeadBundle\Entity\LeadDevice;
|
||||
|
||||
class StatDevice
|
||||
{
|
||||
public const TABLE_NAME = 'email_stats_devices';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
private ?Stat $stat;
|
||||
|
||||
/**
|
||||
* @var LeadDevice|null
|
||||
*/
|
||||
private $device;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateOpened;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(StatDeviceRepository::class)
|
||||
->addIndex(['date_opened'], 'date_opened_search');
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createManyToOne('device', LeadDevice::class)
|
||||
->addJoinColumn('device_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('stat', 'Stat')
|
||||
->addJoinColumn('stat_id', 'id', true, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('dateOpened', 'datetime')
|
||||
->columnName('date_opened')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('stat')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'device',
|
||||
'ipAddress',
|
||||
'stat',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $ip
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ip): void
|
||||
{
|
||||
$this->ipAddress = $ip;
|
||||
}
|
||||
|
||||
public function getStat(): ?Stat
|
||||
{
|
||||
return $this->stat;
|
||||
}
|
||||
|
||||
public function setStat(?Stat $stat): void
|
||||
{
|
||||
$this->stat = $stat;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDateOpened()
|
||||
{
|
||||
return $this->dateOpened;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $dateOpened
|
||||
*/
|
||||
public function setDateOpened($dateOpened): void
|
||||
{
|
||||
$this->dateOpened = $dateOpened;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDevice()
|
||||
{
|
||||
return $this->device;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $device
|
||||
*/
|
||||
public function setDevice(LeadDevice $device): void
|
||||
{
|
||||
$this->device = $device;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<StatDevice>
|
||||
*/
|
||||
class StatDeviceRepository extends CommonRepository
|
||||
{
|
||||
public function getDeviceStats($emailIds, ?\DateTime $fromDate = null, ?\DateTime $toDate = null): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$qb->select('count(es.id) as count, d.device as device, es.list_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats_devices', 'ed')
|
||||
->join('ed', MAUTIC_TABLE_PREFIX.'lead_devices', 'd', 'd.id = ed.device_id')
|
||||
->join('ed', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 'es.id = ed.stat_id');
|
||||
if (null != $emailIds) {
|
||||
if (!is_array($emailIds)) {
|
||||
$emailIds = [(int) $emailIds];
|
||||
}
|
||||
$qb->where(
|
||||
$qb->expr()->in('es.email_id', $emailIds)
|
||||
);
|
||||
}
|
||||
|
||||
$qb->groupBy('es.list_id, d.device');
|
||||
|
||||
if (null !== $fromDate) {
|
||||
// make sure the date is UTC
|
||||
$dt = new DateTimeHelper($fromDate);
|
||||
$qb->andWhere(
|
||||
$qb->expr()->gte('es.date_read', $qb->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
if (null !== $toDate) {
|
||||
// make sure the date is UTC
|
||||
$dt = new DateTimeHelper($toDate);
|
||||
$qb->andWhere(
|
||||
$qb->expr()->lte('es.date_read', $qb->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
|
||||
return $qb->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\EmailBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Stat>
|
||||
*/
|
||||
class StatRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getEmailStatus($trackingHash)
|
||||
{
|
||||
$q = $this->createQueryBuilder('s');
|
||||
$q->select('s')
|
||||
->leftJoin('s.lead', 'l')
|
||||
->leftJoin('s.email', 'e')
|
||||
->where(
|
||||
$q->expr()->eq('s.trackingHash', ':hash')
|
||||
)
|
||||
->setParameter('hash', $trackingHash);
|
||||
$result = $q->getQuery()->getResult();
|
||||
|
||||
return (!empty($result)) ? $result[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $contactId
|
||||
* @param int $emailId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUniqueClickedLinksPerContactAndEmail($contactId, $emailId)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('distinct ph.url, ph.date_hit')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'ph')
|
||||
->where('ph.email_id = :emailId')
|
||||
->andWhere('ph.lead_id = :leadId')
|
||||
->setParameter('leadId', $contactId)
|
||||
->setParameter('emailId', $emailId);
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($result as $row) {
|
||||
$data[$row['date_hit']] = $row['url'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
* @param int|null $createdByUserId
|
||||
* @param int|null $companyId
|
||||
* @param int|null $campaignId
|
||||
* @param int|null $segmentId
|
||||
*/
|
||||
public function getSentEmailToContactData(
|
||||
$limit,
|
||||
\DateTime $dateFrom,
|
||||
\DateTime $dateTo,
|
||||
$createdByUserId = null,
|
||||
$companyId = null,
|
||||
$campaignId = null,
|
||||
$segmentId = null,
|
||||
): array {
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->select('s.id, s.lead_id, s.email_address, s.is_read, s.email_id, s.date_sent, s.date_read')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 's')
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'emails', 'e', 's.email_id = e.id')
|
||||
->addSelect('e.name AS email_name')
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'ph.source = \'email\' and ph.source_id = s.email_id and ph.lead_id = s.lead_id')
|
||||
->addSelect('COUNT(ph.id) AS link_hits');
|
||||
|
||||
if (null !== $createdByUserId) {
|
||||
$q->andWhere('e.created_by = :userId')
|
||||
->setParameter('userId', $createdByUserId);
|
||||
}
|
||||
|
||||
$q->andWhere('s.date_sent BETWEEN :dateFrom AND :dateTo')
|
||||
->setParameter('dateFrom', $dateFrom->format('Y-m-d H:i:s'))
|
||||
->setParameter('dateTo', $dateTo->format('Y-m-d H:i:s'));
|
||||
|
||||
$companyJoinOnExpr = $q->expr()->and(
|
||||
$q->expr()->eq('s.lead_id', 'cl.lead_id')
|
||||
);
|
||||
if (!empty($companyId)) {
|
||||
// Must force a one to one relationship
|
||||
$companyJoinOnExpr->with(
|
||||
$q->expr()->eq('cl.is_primary', 1)
|
||||
);
|
||||
}
|
||||
|
||||
$q->leftJoin('s', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', $companyJoinOnExpr)
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'companies', 'c', 'cl.company_id = c.id')
|
||||
->addSelect('c.id AS company_id')
|
||||
->addSelect('c.companyname AS company_name');
|
||||
|
||||
if (!empty($companyId)) {
|
||||
$q->andWhere('cl.company_id = :companyId')
|
||||
->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
$q->leftJoin('s', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 's.source = "campaign.event" and s.source_id = ce.id')
|
||||
->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id')
|
||||
->addSelect('campaign.id AS campaign_id')
|
||||
->addSelect('campaign.name AS campaign_name');
|
||||
|
||||
if (null !== $campaignId) {
|
||||
$q->andWhere('ce.campaign_id = :campaignId')
|
||||
->setParameter('campaignId', $campaignId);
|
||||
}
|
||||
|
||||
$q->leftJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 's.list_id = ll.id')
|
||||
->addSelect('ll.id AS segment_id')
|
||||
->addSelect('ll.name AS segment_name');
|
||||
|
||||
if (null !== $segmentId) {
|
||||
$sb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$sb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll')
|
||||
->where(
|
||||
$sb->expr()->and(
|
||||
$sb->expr()->eq('lll.leadlist_id', ':segmentId'),
|
||||
$sb->expr()->eq('lll.lead_id', 'ph.lead_id'),
|
||||
$sb->expr()->eq('lll.manually_removed', 0)
|
||||
)
|
||||
);
|
||||
|
||||
// Filter for both broadcasts and campaign related segments
|
||||
$q->andWhere(
|
||||
$q->expr()->or(
|
||||
$q->expr()->eq('s.list_id', ':segmentId'),
|
||||
$q->expr()->and(
|
||||
$q->expr()->isNull('s.list_id'),
|
||||
sprintf('EXISTS (%s)', $sb->getSQL())
|
||||
)
|
||||
)
|
||||
)
|
||||
->setParameter('segmentId', $segmentId);
|
||||
}
|
||||
|
||||
$q->setMaxResults($limit);
|
||||
$q->groupBy('s.id');
|
||||
$q->orderBy('s.id', 'DESC');
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,int|string>|int|null $emailIds
|
||||
* @param array<int,int|string>|int|true|null $listId
|
||||
*/
|
||||
public function getSentStats($emailIds, $listId = null): array
|
||||
{
|
||||
if (!is_array($emailIds)) {
|
||||
$emailIds = [(int) $emailIds];
|
||||
}
|
||||
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('s.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 's')
|
||||
->where(
|
||||
$q->expr()->in('s.email_id', $emailIds)
|
||||
);
|
||||
|
||||
if ($listId) {
|
||||
$q->andWhere('s.list_id = :list')
|
||||
->setParameter('list', $listId);
|
||||
}
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
// index by lead
|
||||
$stats = [];
|
||||
foreach ($result as $r) {
|
||||
$stats[$r['lead_id']] = $r['lead_id'];
|
||||
}
|
||||
|
||||
unset($result);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,int|string>|int|null $emailIds
|
||||
* @param array<int,int|string>|int|true|null $listId
|
||||
* @param bool $combined
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getSentCount($emailIds = null, $listId = null, ?ChartQuery $chartQuery = null, $combined = false)
|
||||
{
|
||||
return $this->getStatusCount('is_sent', $emailIds, $listId, $chartQuery, $combined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,int|string>|int|null $emailIds
|
||||
* @param array<int,int|string>|int|null $listId
|
||||
* @param bool $combined
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getReadCount($emailIds = null, $listId = null, ?ChartQuery $chartQuery = null, $combined = false)
|
||||
{
|
||||
return $this->getStatusCount('is_read', $emailIds, $listId, $chartQuery, $combined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,int|string>|int|null $emailIds
|
||||
* @param array<int,int|string>|int|true|null $listId
|
||||
* @param bool $combined
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getFailedCount($emailIds = null, $listId = null, ?ChartQuery $chartQuery = null, $combined = false)
|
||||
{
|
||||
return $this->getStatusCount('is_failed', $emailIds, $listId, $chartQuery, $combined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $column
|
||||
* @param array<int,int|string>|int|null $emailIds
|
||||
* @param array<int,int|string>|int|true|null $listId
|
||||
* @param bool $combined
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getStatusCount($column, $emailIds = null, $listId = null, ?ChartQuery $chartQuery = null, $combined = false)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(s.id) as count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
|
||||
|
||||
if ($emailIds) {
|
||||
if (!is_array($emailIds)) {
|
||||
$emailIds = [(int) $emailIds];
|
||||
}
|
||||
$q->where(
|
||||
$q->expr()->in('s.email_id', $emailIds)
|
||||
);
|
||||
}
|
||||
|
||||
if ($listId) {
|
||||
if (!$combined) {
|
||||
if (true === $listId) {
|
||||
$q->addSelect('s.list_id')
|
||||
->groupBy('s.list_id');
|
||||
} elseif (is_array($listId)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('s.list_id', ':segmentIds')
|
||||
);
|
||||
$q->setParameter('segmentIds', $listId, ArrayParameterType::INTEGER);
|
||||
|
||||
$q->addSelect('s.list_id')
|
||||
->groupBy('s.list_id');
|
||||
} else {
|
||||
$q->andWhere('s.list_id = :list_id')
|
||||
->setParameter('list_id', $listId);
|
||||
}
|
||||
} else {
|
||||
$subQ = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQ->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'list')
|
||||
->andWhere(
|
||||
$q->expr()->and(
|
||||
$q->expr()->in('list.leadlist_id', array_map('intval', $listId)),
|
||||
$q->expr()->eq('list.lead_id', 's.lead_id')
|
||||
)
|
||||
);
|
||||
|
||||
$q->andWhere(sprintf('EXISTS (%s)', $subQ->getSQL()));
|
||||
}
|
||||
}
|
||||
|
||||
if ('is_sent' === $column) {
|
||||
$q->andWhere('s.is_failed = :false')
|
||||
->setParameter('false', false, 'boolean');
|
||||
} else {
|
||||
$q->andWhere($column.' = :true')
|
||||
->setParameter('true', true, 'boolean');
|
||||
}
|
||||
|
||||
if ($chartQuery) {
|
||||
if ('is_read' === $column) {
|
||||
$chartQuery->applyDateFilters($q, 'date_read', 's');
|
||||
} else {
|
||||
$chartQuery->applyDateFilters($q, 'date_sent', 's');
|
||||
}
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
if ((true === $listId || is_array($listId)) && !$combined) {
|
||||
// Return list group of counts
|
||||
$byList = [];
|
||||
foreach ($results as $result) {
|
||||
$byList[$result['list_id']] = $result['count'];
|
||||
}
|
||||
|
||||
return $byList;
|
||||
}
|
||||
|
||||
return (isset($results[0])) ? $results[0]['count'] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,int|string>|int $emailIds
|
||||
*/
|
||||
public function getOpenedRates($emailIds, ?\DateTime $fromDate = null): array
|
||||
{
|
||||
$inIds = (!is_array($emailIds)) ? [$emailIds] : $emailIds;
|
||||
|
||||
$sq = $this->_em->getConnection()->createQueryBuilder();
|
||||
$sq->select('e.email_id, count(e.id) as the_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'e')
|
||||
->where(
|
||||
$sq->expr()->and(
|
||||
$sq->expr()->eq('e.is_failed', ':false'),
|
||||
$sq->expr()->in('e.email_id', $inIds)
|
||||
)
|
||||
)->setParameter('false', false, 'boolean');
|
||||
|
||||
if (null !== $fromDate) {
|
||||
// make sure the date is UTC
|
||||
$dt = new DateTimeHelper($fromDate);
|
||||
$sq->andWhere(
|
||||
$sq->expr()->gte('e.date_sent', $sq->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
$sq->groupBy('e.email_id');
|
||||
|
||||
// get a total number of sent emails first
|
||||
$totalCounts = $sq->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
foreach ($inIds as $id) {
|
||||
$return[$id] = [
|
||||
'totalCount' => 0,
|
||||
'readCount' => 0,
|
||||
'readRate' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($totalCounts as $t) {
|
||||
if (null != $t['email_id']) {
|
||||
$return[$t['email_id']]['totalCount'] = (int) $t['the_count'];
|
||||
}
|
||||
}
|
||||
|
||||
// now get a read count
|
||||
$sq->andWhere('e.is_read = :true')
|
||||
->setParameter('true', true, 'boolean');
|
||||
$readCounts = $sq->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($readCounts as $r) {
|
||||
$return[$r['email_id']]['readCount'] = (int) $r['the_count'];
|
||||
$return[$r['email_id']]['readRate'] = ($return[$r['email_id']]['totalCount']) ?
|
||||
round(($r['the_count'] / $return[$r['email_id']]['totalCount']) * 100, 2) :
|
||||
0;
|
||||
}
|
||||
|
||||
return (!is_array($emailIds)) ? $return[$emailIds] : $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,int|string>|int $emailIds
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getOpenedStatIds($emailIds = null, $listId = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('s.id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
|
||||
|
||||
if ($emailIds) {
|
||||
if (!is_array($emailIds)) {
|
||||
$emailIds = [(int) $emailIds];
|
||||
}
|
||||
$q->where(
|
||||
$q->expr()->in('s.email_id', $emailIds)
|
||||
);
|
||||
}
|
||||
|
||||
$q->andWhere('open_count > 0');
|
||||
|
||||
if ($listId) {
|
||||
$q->andWhere('s.list_id = '.(int) $listId);
|
||||
}
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's email stat.
|
||||
*
|
||||
* @param int $leadId
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getLeadStats($leadId, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's')
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'emails', 'e', 's.email_id = e.id')
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'email_copies', 'ec', 's.copy_id = ec.id');
|
||||
|
||||
if ($leadId) {
|
||||
$query->andWhere('s.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (!empty($options['basic_select'])) {
|
||||
$query->select(
|
||||
's.email_id, s.id, s.date_read as dateRead, s.date_sent as dateSent, e.subject, e.name as email_name, s.is_read as isRead, s.is_failed as isFailed, ec.subject as storedSubject'
|
||||
);
|
||||
} else {
|
||||
$query->select(
|
||||
's.email_id, s.id, s.date_read as dateRead, s.date_sent as dateSent,e.subject, e.name as email_name, s.is_read as isRead, s.is_failed as isFailed, s.viewed_in_browser as viewedInBrowser, s.retry_count as retryCount, s.list_id, l.name as list_name, s.tracking_hash as idHash, s.open_details as openDetails, ec.subject as storedSubject, s.lead_id'
|
||||
)
|
||||
->leftJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'l', 's.list_id = l.id');
|
||||
}
|
||||
|
||||
$timestampColumn = 's.date_sent';
|
||||
|
||||
if (isset($options['state'])) {
|
||||
$state = $options['state'];
|
||||
if ('read' == $state) {
|
||||
$timestampColumn = 's.date_read';
|
||||
$query->andWhere(
|
||||
$query->expr()->eq('s.is_read', 1)
|
||||
);
|
||||
} elseif ('failed' == $state) {
|
||||
$query->andWhere(
|
||||
$query->expr()->eq('s.is_failed', 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->or(
|
||||
$query->expr()->like('ec.subject', ':search'),
|
||||
$query->expr()->like('e.subject', ':search'),
|
||||
$query->expr()->like('e.name', ':search')
|
||||
)
|
||||
)->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
if (isset($options['fromDate']) && $options['fromDate']) {
|
||||
$dt = new DateTimeHelper($options['fromDate']);
|
||||
$query->andWhere(
|
||||
$query->expr()->gte($timestampColumn, ':fromDate')
|
||||
)->setParameter('fromDate', $dt->toUtcString());
|
||||
}
|
||||
|
||||
$timeToReadParser = function (&$stat): void {
|
||||
$dateSent = new DateTimeHelper($stat['dateSent']);
|
||||
if (!empty($stat['dateSent']) && !empty($stat['dateRead'])) {
|
||||
$stat['timeToRead'] = $dateSent->getDiff($stat['dateRead']);
|
||||
} else {
|
||||
$stat['timeToRead'] = false;
|
||||
}
|
||||
};
|
||||
|
||||
return $this->getTimelineResults(
|
||||
$query,
|
||||
$options,
|
||||
'storedSubject, e.subject',
|
||||
$timestampColumn,
|
||||
['openDetails'],
|
||||
['dateRead', 'dateSent'],
|
||||
$timeToReadParser,
|
||||
's.id'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get counts for Sent, Read and Failed emails.
|
||||
*
|
||||
* @param QueryBuilder $query
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getIgnoredReadFailed($query = null)
|
||||
{
|
||||
$query->select('count(es.id) as sent, count(CASE WHEN es.is_read THEN 1 ELSE null END) as "read", count(CASE WHEN es.is_failed THEN 1 ELSE null END) as failed');
|
||||
|
||||
$results = $query->executeQuery()->fetchAssociative();
|
||||
|
||||
if ($results) {
|
||||
$results['ignored'] = $results['sent'] - $results['read'] - $results['failed'];
|
||||
unset($results['sent']);
|
||||
} else {
|
||||
$results['ignored'] = $results['sent'] = $results['read'] = $results['failed'] = 0;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pie graph data for Sent, Read and Failed email count.
|
||||
*
|
||||
* @param QueryBuilder $query
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getMostEmails($query, $limit = 10, $offset = 0): array
|
||||
{
|
||||
$query
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sent counts based grouped by email Id.
|
||||
*
|
||||
* @param array $emailIds
|
||||
*/
|
||||
public function getSentCounts($emailIds = [], ?\DateTime $fromDate = null): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->select('e.email_id, count(e.id) as sentcount')
|
||||
->from(MAUTIC_TABLE_PREFIX.'email_stats', 'e')
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->in('e.email_id', $emailIds),
|
||||
$q->expr()->eq('e.is_failed', ':false')
|
||||
)
|
||||
)->setParameter('false', false, 'boolean');
|
||||
|
||||
if (null !== $fromDate) {
|
||||
// make sure the date is UTC
|
||||
$dt = new DateTimeHelper($fromDate);
|
||||
$q->andWhere(
|
||||
$q->expr()->gte('e.date_read', $q->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
$q->groupBy('e.email_id');
|
||||
|
||||
// get a total number of sent emails first
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$counts = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$counts[$r['email_id']] = $r['sentcount'];
|
||||
}
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'email_stats')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a stat.
|
||||
*/
|
||||
public function deleteStat($id): void
|
||||
{
|
||||
$this->getEntityManager()->getConnection()->delete(MAUTIC_TABLE_PREFIX.'email_stats', ['id' => (int) $id]);
|
||||
}
|
||||
|
||||
public function deleteStats(array $ids): void
|
||||
{
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$qb->delete(MAUTIC_TABLE_PREFIX.'email_stats')
|
||||
->where(
|
||||
$qb->expr()->in('id', $ids)
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 's';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function findContactEmailStats($leadId, $emailId)
|
||||
{
|
||||
return $this->createQueryBuilder('s')
|
||||
->where('IDENTITY(s.lead) = :leadId AND IDENTITY(s.email) = :emailId')
|
||||
->setParameter('leadId', (int) $leadId)
|
||||
->setParameter('emailId', (int) $emailId)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function checkContactSentEmail(int $contactId, int $emailId): bool
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
|
||||
$query->select('1')
|
||||
->where('s.email_id = :emailId')
|
||||
->andWhere('s.lead_id = :contactId')
|
||||
->andWhere('is_failed = 0')
|
||||
->setParameter('emailId', $emailId)
|
||||
->setParameter('contactId', $contactId)
|
||||
->setMaxResults(1);
|
||||
|
||||
return (bool) $query->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Formatted as [contactId => sentCount]
|
||||
*/
|
||||
public function getSentCountForContacts(array $contacts, $emailId): array
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
|
||||
$query->select('count(s.id) as sent_count, s.lead_id')
|
||||
->where('s.email_id = :email')
|
||||
->andWhere('s.lead_id in (:contacts)')
|
||||
->andWhere('s.is_failed = 0')
|
||||
->setParameter('email', $emailId)
|
||||
->setParameter('contacts', $contacts, ArrayParameterType::INTEGER)
|
||||
->groupBy('s.lead_id');
|
||||
|
||||
$results = $query->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$contacts = [];
|
||||
foreach ($results as $result) {
|
||||
$contacts[$result['lead_id']] = $result['sent_count'];
|
||||
}
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $contacts
|
||||
*
|
||||
* @return array<int, array<string, int|float>>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getStatsSummaryForContacts(array $contacts): array
|
||||
{
|
||||
$queryBuilder = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQueryBuilder = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$leadAlias = 'l'; // leads
|
||||
$statsAlias = 'es'; // email_stats
|
||||
$subQueryAlias = 'sq'; // sub query
|
||||
$cutAlias = 'cut'; // channel_url_trackables
|
||||
$pageHitsAlias = 'ph'; // page_hits
|
||||
|
||||
// use sub query to get page hits for and unique page hits selected contacts
|
||||
$subQueryBuilder->select(
|
||||
"COUNT({$pageHitsAlias}.id) AS hits",
|
||||
"COUNT(DISTINCT({$pageHitsAlias}.redirect_id)) AS unique_hits",
|
||||
"{$cutAlias}.channel_id",
|
||||
"{$pageHitsAlias}.lead_id"
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'channel_url_trackables', $cutAlias)
|
||||
->join(
|
||||
$cutAlias,
|
||||
MAUTIC_TABLE_PREFIX.'page_hits',
|
||||
$pageHitsAlias,
|
||||
"{$cutAlias}.redirect_id = {$pageHitsAlias}.redirect_id AND {$cutAlias}.channel_id = {$pageHitsAlias}.source_id"
|
||||
)
|
||||
->where("{$cutAlias}.channel = 'email' AND {$pageHitsAlias}.source = 'email'")
|
||||
->andWhere("{$pageHitsAlias}.lead_id in (:contacts)")
|
||||
->setParameter('contacts', $contacts, ArrayParameterType::INTEGER)
|
||||
->groupBy("{$cutAlias}.channel_id, {$pageHitsAlias}.lead_id");
|
||||
|
||||
// main query
|
||||
$queryBuilder->select(
|
||||
"{$leadAlias}.id AS `lead_id`",
|
||||
"COUNT({$statsAlias}.id) AS `sent_count`",
|
||||
"SUM(IF({$statsAlias}.is_read IS NULL, 0, {$statsAlias}.is_read)) AS `read_count`",
|
||||
"SUM(IF({$subQueryAlias}.hits is NULL, 0, 1)) AS `clicked_through_count`",
|
||||
)->from(MAUTIC_TABLE_PREFIX.'email_stats', $statsAlias)
|
||||
->rightJoin(
|
||||
$statsAlias,
|
||||
MAUTIC_TABLE_PREFIX.'leads',
|
||||
$leadAlias,
|
||||
"{$statsAlias}.lead_id=l.id"
|
||||
)->leftJoin(
|
||||
$statsAlias,
|
||||
"({$subQueryBuilder->getSQL()})",
|
||||
$subQueryAlias,
|
||||
"{$statsAlias}.email_id = {$subQueryAlias}.channel_id AND {$statsAlias}.lead_id = {$subQueryAlias}.lead_id"
|
||||
)->andWhere("{$leadAlias}.id in (:contacts)")
|
||||
->setParameter('contacts', $contacts, ArrayParameterType::INTEGER)
|
||||
->groupBy("{$leadAlias}.id");
|
||||
|
||||
$results = $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$contacts = [];
|
||||
foreach ($results as $result) {
|
||||
$sentCount = (int) $result['sent_count'];
|
||||
$readCount = (int) $result['read_count'];
|
||||
$clickedCount = (int) $result['clicked_through_count'];
|
||||
|
||||
$contacts[(int) $result['lead_id']] = [
|
||||
'sent_count' => $sentCount,
|
||||
'read_count' => $readCount,
|
||||
'clicked_count' => $clickedCount,
|
||||
'open_rate' => round($sentCount > 0 ? ($readCount / $sentCount) : 0, 4),
|
||||
'click_through_rate' => round($sentCount > 0 ? ($clickedCount / $sentCount) : 0, 4),
|
||||
'click_through_open_rate' => round($readCount > 0 ? ($clickedCount / $readCount) : 0, 4),
|
||||
];
|
||||
}
|
||||
|
||||
return $contacts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string> $emailsIds
|
||||
* @param array<int> $eventsIds
|
||||
*
|
||||
* @return array<int, array<string, int|string>>
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getStatsSummaryByCountry(\DateTimeImmutable $dateFrom, \DateTimeImmutable $dateTo, array $emailsIds, string $sourceType = 'email', array $eventsIds = []): array
|
||||
{
|
||||
$queryBuilder = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQueryBuilder = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$leadAlias = 'l'; // leads
|
||||
$statsAlias = 'es'; // email_stats
|
||||
$subQueryAlias = 'sq'; // sub query
|
||||
$cutAlias = 'cut'; // channel_url_trackables
|
||||
$pageHitsAlias = 'ph'; // page_hits
|
||||
|
||||
// use sub query to get page hits for and unique page hits selected contacts
|
||||
$subQueryBuilder->select(
|
||||
"COUNT({$pageHitsAlias}.id) AS hits",
|
||||
"COUNT(DISTINCT({$pageHitsAlias}.redirect_id)) AS unique_hits",
|
||||
"{$cutAlias}.channel_id",
|
||||
"{$pageHitsAlias}.lead_id"
|
||||
)
|
||||
->from(MAUTIC_TABLE_PREFIX.'channel_url_trackables', $cutAlias)
|
||||
->join(
|
||||
$cutAlias,
|
||||
MAUTIC_TABLE_PREFIX.'page_hits',
|
||||
$pageHitsAlias,
|
||||
"{$cutAlias}.redirect_id = {$pageHitsAlias}.redirect_id AND {$cutAlias}.channel_id = {$pageHitsAlias}.source_id"
|
||||
)
|
||||
->where("{$cutAlias}.channel = 'email' AND {$pageHitsAlias}.source = 'email'")
|
||||
->andWhere("{$cutAlias}.channel_id in (:emails)")
|
||||
->groupBy("{$cutAlias}.channel_id, {$pageHitsAlias}.lead_id");
|
||||
|
||||
// main query
|
||||
$queryBuilder->addSelect(
|
||||
"COUNT({$statsAlias}.id) AS `sent_count`",
|
||||
"SUM(IF({$statsAlias}.is_read IS NULL, 0, {$statsAlias}.is_read)) AS `read_count`",
|
||||
"SUM(IF({$subQueryAlias}.hits is NULL, 0, 1)) AS `clicked_through_count`",
|
||||
)->from(MAUTIC_TABLE_PREFIX.'email_stats', $statsAlias)
|
||||
->rightJoin(
|
||||
$statsAlias,
|
||||
MAUTIC_TABLE_PREFIX.'leads',
|
||||
$leadAlias,
|
||||
"{$statsAlias}.lead_id=l.id"
|
||||
)->leftJoin(
|
||||
$statsAlias,
|
||||
"({$subQueryBuilder->getSQL()})",
|
||||
$subQueryAlias,
|
||||
"{$statsAlias}.email_id = {$subQueryAlias}.channel_id AND {$statsAlias}.lead_id = {$subQueryAlias}.lead_id"
|
||||
);
|
||||
|
||||
switch ($sourceType) {
|
||||
case 'campaign':
|
||||
$queryBuilder->addSelect("{$leadAlias}.country AS `country`")
|
||||
->andWhere("{$statsAlias}.source_id in (:events)")
|
||||
->andWhere("{$statsAlias}.source = :source")
|
||||
->setParameter('emails', $emailsIds, ArrayParameterType::INTEGER)
|
||||
->setParameter('events', $eventsIds, ArrayParameterType::INTEGER)
|
||||
->setParameter('source', 'campaign.event');
|
||||
break;
|
||||
case 'email':
|
||||
$queryBuilder->addSelect("{$leadAlias}.country AS `country`")
|
||||
->andWhere("{$statsAlias}.email_id in (:emails)")
|
||||
->setParameter('emails', $emailsIds, ArrayParameterType::INTEGER);
|
||||
}
|
||||
|
||||
$queryBuilder->groupBy("{$leadAlias}.country")
|
||||
->orderBy("{$leadAlias}.country", 'ASC');
|
||||
$queryBuilder->andWhere("{$statsAlias}.date_sent BETWEEN :dateFrom AND :dateTo");
|
||||
$queryBuilder->setParameter('dateFrom', $dateFrom->format(DateTimeHelper::FORMAT_DB));
|
||||
$queryBuilder->setParameter('dateTo', $dateTo->setTime(23, 59, 59)->format('Y-m-d H:i:s'));
|
||||
|
||||
return $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user