| Current Path : /var/www/v3.cesa.co.za/src/MailQueueBundle/Service/ |
| Current File : /var/www/v3.cesa.co.za/src/MailQueueBundle/Service/MailQueueService.php |
<?php
namespace App\MailQueueBundle\Service;
use App\MailQueueBundle\Entity\BulkMailMessage;
use App\MailQueueBundle\Entity\BulkMailToSend;
use App\MailQueueBundle\Model\MailQueueMessageRow;
use App\MailQueueBundle\Model\MailQueueSummary;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class MailQueueService
{
public function __construct(
#[Autowire(service: 'doctrine.orm.mail_entity_manager')]
private readonly EntityManagerInterface $mailEntityManager,
) {
}
/**
* @param array{pending: int|string, sent: int|string, failed: int|string} $counts
*
* @return array{pending: int, sent: int, failed: int}
*/
public static function aggregateSendCounts(array $counts): array
{
return [
'pending' => (int) ($counts['pending'] ?? 0),
'sent' => (int) ($counts['sent'] ?? 0),
'failed' => (int) ($counts['failed'] ?? 0),
];
}
public static function statusLabel(int $sent): string
{
return match ($sent) {
BulkMailToSend::STATUS_SENT => 'Sent',
BulkMailToSend::STATUS_FAILED => 'Failed',
default => 'Pending',
};
}
public static function statusCssClass(int $sent): string
{
return match ($sent) {
BulkMailToSend::STATUS_SENT => 'success',
BulkMailToSend::STATUS_FAILED => 'danger',
default => 'warning',
};
}
public function getGlobalSummary(): MailQueueSummary
{
$statusCounts = $this->mailEntityManager->createQueryBuilder()
->select('t.sent AS status, COUNT(t.toSendId) AS cnt')
->from(BulkMailToSend::class, 't')
->groupBy('t.sent')
->getQuery()
->getResult();
$pending = 0;
$sent = 0;
$failed = 0;
foreach ($statusCounts as $row) {
$status = (int) $row['status'];
$count = (int) $row['cnt'];
if ($status === BulkMailToSend::STATUS_SENT) {
$sent = $count;
} elseif ($status === BulkMailToSend::STATUS_FAILED) {
$failed = $count;
} else {
$pending += $count;
}
}
$campaignsInProgress = (int) $this->mailEntityManager->createQueryBuilder()
->select('COUNT(m.messageId)')
->from(BulkMailMessage::class, 'm')
->where('m.sendComplete = 0')
->getQuery()
->getSingleScalarResult();
return new MailQueueSummary($campaignsInProgress, $pending, $sent, $failed);
}
/**
* @return list<MailQueueMessageRow>
*/
public function listMessages(int $limit = 100): array
{
$qb = $this->mailEntityManager->createQueryBuilder();
$qb
->select(
'm.messageId AS messageId',
'm.subject AS subject',
'm.sentToGroup AS sentToGroup',
'm.dateSent AS dateSent',
'm.numRecipients AS numRecipients',
'm.sendComplete AS sendComplete',
'm.dateComplete AS dateComplete',
'SUM(CASE WHEN t.sent = 0 THEN 1 ELSE 0 END) AS pending',
'SUM(CASE WHEN t.sent = 1 THEN 1 ELSE 0 END) AS sent',
'SUM(CASE WHEN t.sent = 2 THEN 1 ELSE 0 END) AS failed'
)
->from(BulkMailMessage::class, 'm')
->leftJoin('m.toSendRows', 't')
->groupBy('m.messageId')
->orderBy('m.messageId', 'DESC')
->setMaxResults($limit);
$rows = [];
foreach ($qb->getQuery()->getResult(Query::HYDRATE_ARRAY) as $row) {
$counts = self::aggregateSendCounts([
'pending' => $row['pending'] ?? 0,
'sent' => $row['sent'] ?? 0,
'failed' => $row['failed'] ?? 0,
]);
$rows[] = new MailQueueMessageRow(
(int) $row['messageId'],
$row['subject'] ?? null,
$row['sentToGroup'] ?? null,
$row['dateSent'] ?? null,
isset($row['numRecipients']) ? (int) $row['numRecipients'] : null,
(int) ($row['sendComplete'] ?? 0) === 1,
$row['dateComplete'] ?? null,
$counts['pending'],
$counts['sent'],
$counts['failed'],
);
}
return $rows;
}
public function findMessage(int $messageId): ?BulkMailMessage
{
return $this->mailEntityManager->find(BulkMailMessage::class, $messageId);
}
/**
* Fetch the message body for a campaign (template on Messages, or first personalized ToSend row).
*/
public function findMessageBody(int $messageId): ?string
{
$connection = $this->mailEntityManager->getConnection();
$row = $connection->fetchAssociative(
'SELECT * FROM Messages WHERE MessageID = :id LIMIT 1',
['id' => $messageId]
);
$body = self::extractMessageBodyFromRow(is_array($row) ? $row : null);
if ($body !== null) {
return $body;
}
$toSendBody = $connection->fetchOne(
'SELECT Message FROM ToSend
WHERE MessageID = :id AND Message IS NOT NULL AND TRIM(Message) <> \'\'
ORDER BY ToSendID ASC LIMIT 1',
['id' => $messageId]
);
if (is_string($toSendBody) && trim($toSendBody) !== '') {
return $toSendBody;
}
return null;
}
/**
* Build a full HTML document for safe display inside a sandboxed iframe srcdoc attribute.
*/
public static function buildMessageBodySrcdoc(?string $body): string
{
if ($body === null || trim($body) === '') {
return '';
}
if (self::looksLikeHtmlMessage($body)) {
if (stripos($body, '<html') !== false) {
return $body;
}
return '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>' . $body . '</body></html>';
}
return '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="font-family:Arial,sans-serif;font-size:13px;margin:8px;white-space:pre-wrap;">'
. htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
. '</body></html>';
}
/**
* @param array<string, mixed>|null $row
*/
private static function extractMessageBodyFromRow(?array $row): ?string
{
if ($row === null || count($row) === 0) {
return null;
}
$normalized = [];
foreach ($row as $key => $value) {
if (!is_string($key) && !is_int($key)) {
continue;
}
$normalized[strtolower((string) $key)] = $value;
}
$candidates = [
'message',
'messagebody',
'messagetext',
'body',
'htmlbody',
'textbody',
'bodyhtml',
'bodytext',
'mailbody',
'mailtext',
];
foreach ($candidates as $column) {
if (!isset($normalized[$column]) || !is_string($normalized[$column])) {
continue;
}
if (trim($normalized[$column]) !== '') {
return $normalized[$column];
}
}
return null;
}
private static function looksLikeHtmlMessage(string $body): bool
{
return preg_match('/<\s*(html|body|head|p|div|br|table|span|a|ul|ol|li)\b/i', $body) === 1;
}
/**
* @return Query
*/
public function getToSendQueryForMessage(int $messageId): Query
{
return $this->mailEntityManager->createQueryBuilder()
->select('t')
->from(BulkMailToSend::class, 't')
->innerJoin('t.message', 'm')
->where('m.messageId = :messageId')
->setParameter('messageId', $messageId)
->orderBy('t.sent', 'ASC')
->addOrderBy('t.toSendId', 'ASC')
->getQuery();
}
}