<?php
declare(strict_types=1);
namespace App\Service;
use App\Exception\ApiException\ApiEmailSearchException;
use App\Exception\ApiException\ApiEmailSendException;
use App\Exception\ApiException\ApiValidationException;
use App\Exception\ApiException\NotFoundException;
use App\Exception\ApiException\SendGridApiException;
use App\Model\Email;
use App\Validator\TemplatePath;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\BodyRendererInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
class EmailService
{
/** @var LoggerInterface */
protected $log;
/** @var MailerInterface */
private $mailer;
/** @var Translator */
private $translator;
/** @var BodyRendererInterface */
private $bodyRenderer;
/** @var ValidatorInterface */
private $validator;
/** @var Environment */
private $twig;
/** @var KernelInterface */
private $kernel;
/** @var SendGridApi */
private $sendGridApi;
public function __construct(
LoggerInterface $log,
MailerInterface $mailer,
TranslatorInterface $translator,
BodyRendererInterface $bodyRenderer,
ValidatorInterface $validator,
Environment $twig,
KernelInterface $kernel,
SendGridApi $sendGridApi
) {
$this->log = $log;
$this->mailer = $mailer;
$this->translator = $translator;
$this->bodyRenderer = $bodyRenderer;
$this->validator = $validator;
$this->twig = $twig;
$this->kernel = $kernel;
$this->sendGridApi = $sendGridApi;
}
/**
* @param TemplatedEmail $templatedEmail
* @throws ApiEmailSendException
*/
public function send(TemplatedEmail $templatedEmail): void
{
try {
$this->mailer->send($templatedEmail);
} catch (TransportExceptionInterface $e) {
$this->log->error(get_class($e).' - '.$e->getMessage());
throw new ApiEmailSendException($e->getMessage());
}
}
/**
* @param string $query
* @param int $limit
*
* @return array
* @throws ApiEmailSearchException
*/
public function findMessagesByQuery(string $query, int $limit = 0): array
{
try {
if ($limit > 0 && $limit <= SendGridApi::FIND_MESSAGES_MAX_LIMIT) {
return $this->sendGridApi->findMessagesByQuery($query, $limit);
}
// init first batch
$messages = $messagesId = [];
$batchQuery = $query;
$batchCpt = 0;
do {
++$batchCpt;
// messages recovery for current batch
$tmpMessages = [];
// get messages for current batch
$this->log->info(sprintf(
'Batch #%d: Try to get %d messages by API (%d already recovered)...',
$batchCpt,
SendGridApi::FIND_MESSAGES_MAX_LIMIT,
count($messages),
));
$this->log->debug(sprintf(
'Batch #%d: limit = %d, query = %s...',
$batchCpt,
$limit,
$batchQuery,
));
$batchMessages = $this->sendGridApi->findMessagesByQuery($batchQuery, SendGridApi::FIND_MESSAGES_MAX_LIMIT);
$this->log->debug(sprintf(
'Batch #%d: %d messages recovered by API.',
$batchCpt,
count($batchMessages),
));
// filter messages already recovery by previous batch
foreach ($batchMessages as $batchMessage) {
if (in_array($batchMessage['msg_id'], $messagesId)) {
continue;
}
$messagesId[] = $batchMessage['msg_id'];
$tmpMessages[] = $batchMessage;
}
$this->log->debug(sprintf(
'Batch #%d: %d messages after remove duplicate messages.',
$batchCpt,
count($tmpMessages),
));
// finish here if no new messages recovered. Avoid infinite loop
if (!$tmpMessages) {
$this->log->debug(sprintf('Batch #%d: [STOP] No new messages recovered', $batchCpt));
break;
}
// determine period for next batch, by last event time.
// Messages return by API sorted by last event time in descending order
$batchPeriodEnd = $tmpMessages[array_key_last($tmpMessages)]['last_event_time'];
$batchPeriodStart = (new \DateTime($batchPeriodEnd))->modify('-1 month')->format(\DateTime::ATOM);
// build query for next batch
$batchQuery = 'last_event_time BETWEEN TIMESTAMP "'.$batchPeriodStart.'" AND TIMESTAMP "'.$batchPeriodEnd.'"';
if ($query) {
$batchQuery = $query.' AND '.$batchQuery;
}
$messages = array_merge($messages, $tmpMessages);
if ($limit > 0 && count($messages) >= $limit) {
$this->log->debug(sprintf('Batch #%d: [STOP] Message limit is reached', $batchCpt));
$messages = array_slice($messages, 0, $limit);
break;
}
} while (SendGridApi::FIND_MESSAGES_MAX_LIMIT === count($batchMessages));
$this->log->info(sprintf(
'[OK] %d messages recovered by API in %d batch',
count($messages),
$batchCpt,
));
return $messages;
} catch (SendGridApiException $e) {
$this->log->error(get_class($e).' - '.$e->getMessage());
throw new ApiEmailSearchException($e->getMessage());
}
}
/**
* @param string $messageId
*
* @return array
* @throws ApiEmailSearchException
* @throws NotFoundException
*/
public function findMessageById(string $messageId): array
{
try {
return $this->sendGridApi->findMessageById($messageId);
} catch (SendGridApiException $e) {
$this->log->error(get_class($e).' - '.$e->getMessage());
throw new ApiEmailSearchException($e->getMessage());
}
}
/**
* @param Email $email
* @return TemplatedEmail
* @throws ApiValidationException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
*/
public function generateTemplatedEmail(Email $email): TemplatedEmail
{
$errors = $this->validator->validate($email);
if (count($errors) > 0) {
throw new ApiValidationException($errors);
}
$templatedEmail = (new TemplatedEmail())
->from($this->buildFromAddress($email->getFrom(), $email->getFromName()))
->to(...Address::createArray($email->getTo()))
->htmlTemplate(TemplatePath::getTemplateRealPath($email->getTemplatePath()))
->context(array_merge($email->getTemplateData(), ['email_locale' => $email->getLocale()]));
if (is_array($email->getCc())) {
$templatedEmail->cc(...Address::createArray($email->getCc()));
}
if (is_array($email->getBcc())) {
$templatedEmail->bcc(...Address::createArray($email->getBcc()));
}
if (is_array($email->getReply())) {
$templatedEmail->replyTo(...Address::createArray($email->getReply()));
}
$attachments = $email->getAttachments();
if (is_array($attachments)) {
foreach ($attachments as $attachment) {
$fileString = base64_decode($attachment['content']);
$templatedEmail->attach($fileString, $attachment['filename'], $attachment['contentType']);
}
}
// Set locale for email template rendering
$originalLocale = $this->translator->getLocale();
if (!is_null($email->getLocale())) {
$this->translator->setLocale($email->getLocale());
}
// Set default email subject
if (is_null($email->getSubject())) {
$subject = trim($this->twig->render(
TemplatePath::getSubjectTemplateRealPath($email->getTemplatePath()),
$email->getTemplateData()
));
$templatedEmail->subject($subject);
} else {
$templatedEmail->subject($email->getSubject());
}
// Render email
$this->bodyRenderer->render($templatedEmail);
// Reset locale
$this->translator->setLocale($originalLocale);
return $templatedEmail;
}
/**
* @param string $from
* @param string $fromName
* @return Address
* @throws \Exception
*/
private function buildFromAddress(string $from, string $fromName): Address
{
switch ($this->kernel->getEnvironment()) {
case 'prod':
return new Address("$from@hipay.com", $fromName);
case 'stage':
return new Address("$from@stage.hipay.com", "$fromName STAGE");
case 'qa':
return new Address("$from@qa.hipay.com", "$fromName QA");
case 'test':
return new Address("$from@test.hipay.com", "$fromName TEST");
case 'dev':
case 'devgcp':
return new Address("$from@dev.hipay.com", "$fromName DEV");
default:
throw new \Exception('Unsuppported environment');
}
}
}