Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -0,0 +1,335 @@
<?php
namespace Mautic\EmailBundle\Controller;
use Mautic\AssetBundle\Model\AssetModel;
use Mautic\CacheBundle\Cache\CacheProvider;
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
use Mautic\CoreBundle\Controller\AjaxLookupControllerTrait;
use Mautic\CoreBundle\Controller\VariantAjaxControllerTrait;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\EmailBundle\Helper\PlainTextHelper;
use Mautic\EmailBundle\Mailer\Message\MauticMessage;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\EmailBundle\MonitoredEmail\Mailbox;
use Mautic\EmailBundle\Stats\EmailDependencies;
use Mautic\PageBundle\Form\Type\AbTestPropertiesType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Address;
use Twig\Environment;
class AjaxController extends CommonAjaxController
{
use VariantAjaxControllerTrait;
use AjaxLookupControllerTrait;
public function getAbTestFormAction(Request $request, FormFactoryInterface $formFactory, EmailModel $emailModel, Environment $twig): JsonResponse
{
return $this->sendJsonResponse($this->getAbTestForm(
$request,
$emailModel,
fn ($formType, $formOptions) => $formFactory->create(AbTestPropertiesType::class, [], ['formType' => $formType, 'formTypeOptions' => $formOptions]),
fn ($form) => $this->renderView('@MauticEmail/AbTest/form.html.twig', ['form' => $this->setFormTheme($form, $twig, ['@MauticEmail/AbTest/form.html.twig', '@MauticEmail/FormTheme/Email/layout.html.twig'])]),
'email_abtest_settings',
'emailform'
));
}
public function sendBatchAction(Request $request): JsonResponse
{
$dataArray = ['success' => 0];
/** @var EmailModel $model */
$model = $this->getModel('email');
$objectId = $request->request->get('id', 0);
$pending = $request->request->get('pending', 0);
$limit = $request->request->get('batchlimit', 100);
if ($objectId && $entity = $model->getEntity($objectId)) {
$dataArray['success'] = 1;
$session = $request->getSession();
$progress = $session->get('mautic.email.send.progress', [0, (int) $pending]);
$stats = $session->get('mautic.email.send.stats', ['sent' => 0, 'failed' => 0, 'failedRecipients' => []]);
$inProgress = $session->get('mautic.email.send.active', false);
if ($pending && !$inProgress && $entity->isPublished()) {
$session->set('mautic.email.send.active', true);
[$batchSentCount, $batchFailedCount, $batchFailedRecipients] = $model->sendEmailToLists($entity, null, $limit);
$progress[0] += ($batchSentCount + $batchFailedCount);
$stats['sent'] += $batchSentCount;
$stats['failed'] += $batchFailedCount;
foreach ($batchFailedRecipients as $emails) {
$stats['failedRecipients'] = $stats['failedRecipients'] + $emails;
}
$session->set('mautic.email.send.progress', $progress);
$session->set('mautic.email.send.stats', $stats);
$session->set('mautic.email.send.active', false);
}
$dataArray['percent'] = ($progress[1]) ? ceil(($progress[0] / $progress[1]) * 100) : 100;
$dataArray['progress'] = $progress;
$dataArray['stats'] = $stats;
}
return $this->sendJsonResponse($dataArray);
}
/**
* Called by parent::getBuilderTokensAction().
*
* @return array
*/
protected function getBuilderTokens($query)
{
/** @var EmailModel $model */
$model = $this->getModel('email');
return $model->getBuilderComponents(null, ['tokens'], (string) $query);
}
public function generatePlaintTextAction(Request $request): JsonResponse
{
$custom = $request->request->get('custom');
$parser = new PlainTextHelper(
[
'base_url' => $request->getSchemeAndHttpHost().$request->getBasePath(),
]
);
$dataArray = [
'text' => $parser->setHtml($custom)->getText(),
];
return $this->sendJsonResponse($dataArray);
}
public function getAttachmentsSizeAction(Request $request, AssetModel $assetModel): JsonResponse
{
$assets = $request->query->all()['assets'] ?? [];
$size = 0;
if ($assets) {
$size = $assetModel->getTotalFilesize($assets);
}
return $this->sendJsonResponse(['size' => $size]);
}
/**
* Tests monitored email connection settings.
*/
public function testMonitoredEmailServerConnectionAction(Request $request, Mailbox $mailbox): JsonResponse
{
$dataArray = ['success' => 0, 'message' => ''];
if ($this->user->isAdmin()) {
$settings = $request->request->all();
if (empty($settings['password'])) {
$existingMonitoredSettings = $this->coreParametersHelper->get('monitored_email');
if (is_array($existingMonitoredSettings) && (!empty($existingMonitoredSettings[$settings['mailbox']]['password']))) {
$settings['password'] = $existingMonitoredSettings[$settings['mailbox']]['password'];
}
}
try {
$mailbox->setMailboxSettings($settings);
$folders = $mailbox->getListingFolders();
if (!empty($folders)) {
$dataArray['folders'] = '';
foreach ($folders as $folder) {
$dataArray['folders'] .= "<option value=\"$folder\">$folder</option>\n";
}
}
$dataArray['success'] = 1;
$dataArray['message'] = $this->translator->trans('mautic.core.success');
} catch (\Exception $e) {
$dataArray['message'] = $this->translator->trans($e->getMessage());
}
}
return $this->sendJsonResponse($dataArray);
}
public function sendTestEmailAction(TransportInterface $transport, UserHelper $userHelper, CoreParametersHelper $parametersHelper): Response
{
$user = $userHelper->getUser();
$email = (new MauticMessage())
->subject($this->translator->trans('mautic.email.config.mailer.transport.test_send.subject'))
->text($this->translator->trans('mautic.email.config.mailer.transport.test_send.body'))
->from(new Address($parametersHelper->get('mailer_from_email'), $parametersHelper->get('mailer_from_name') ?: ''))
->to(new Address($user->getEmail(), trim($user->getFirstName().' '.$user->getLastName()) ?: ''));
$success = 1;
$message = $this->translator->trans('mautic.core.success');
try {
$transport->send($email);
} catch (TransportExceptionInterface $e) {
$success = 0;
$message = $e->getMessage();
}
return $this->sendJsonResponse(['success' => $success, 'message' => $message]);
}
public function getEmailCountStatsAction(Request $request): JsonResponse
{
/** @var EmailModel $model */
$model = $this->getModel('email');
$id = $request->query->get('id');
$ids = $request->query->all()['ids'] ?? [];
// Support for legacy calls
if (!$ids && $id) {
$ids = [$id];
}
$data = [];
foreach ($ids as $id) {
if ($email = $model->getEntity($id)) {
$pending = $model->getPendingLeads($email, null, true);
$queued = $model->getQueuedCounts($email);
$data[] = [
'id' => $email->getId(),
'pending' => 'list' === $email->getEmailType() && $pending ? $this->translator->trans(
'mautic.email.stat.leadcount',
['%count%' => $pending]
) : 0,
'queued' => ($queued) ? $this->translator->trans('mautic.email.stat.queued', ['%count%' => $queued]) : 0,
'sentCount' => $this->translator->trans('mautic.email.stat.sentcount', ['%count%' => $email->getSentCount(true)]),
'readCount' => $this->translator->trans('mautic.email.stat.readcount', ['%count%' => $email->getReadCount(true)]),
'readPercent' => $this->translator->trans('mautic.email.stat.readpercent', ['%count%' => $email->getReadPercentage(true)]),
];
}
}
// Support for legacy calls
if ($request->get('id') && !empty($data[0])) {
$data = $data[0];
} else {
$data = [
'success' => 1,
'stats' => $data,
];
}
return new JsonResponse($data);
}
public function getEmailDeliveredCountAction(Request $request, CacheProvider $cacheProvider): JsonResponse
{
$emailId = (int) InputHelper::clean($request->query->get('id'));
if (0 === $emailId) {
return $this->sendJsonResponse([
'success' => 0,
'message' => $this->translator->trans('mautic.core.error.badrequest'),
], 400);
}
$cacheTimeout = (int) $this->coreParametersHelper->get('cached_data_timeout');
$cacheItem = $cacheProvider->getItem('email.stats.delivered.'.$emailId);
if ($cacheItem->isHit()) {
$deliveredCount = $cacheItem->get();
} else {
/** @var EmailModel $model */
$model = $this->getModel('email');
$email = $model->getEntity($emailId);
if (null === $email) {
return $this->sendJsonResponse([
'success' => 0,
'message' => $this->translator->trans('mautic.api.call.notfound'),
], 404);
}
$deliveredCount = $model->getDeliveredCount($email);
$cacheItem->set($deliveredCount);
$cacheItem->expiresAfter($cacheTimeout * 60);
$cacheProvider->save($cacheItem);
}
return $this->sendJsonResponse([
'success' => 1,
'delivered' => $deliveredCount,
]);
}
public function heatmapAction(Request $request, EmailModel $model): JsonResponse
{
$emailId = (int) $request->query->get('id');
$email = $model->getEntity($emailId);
if (null === $email) {
return $this->sendJsonResponse([
'message' => $this->translator->trans('mautic.api.call.notfound'),
], 404);
}
if (!$this->security->hasEntityAccess(
'email:emails:viewown',
'email:emails:viewother',
$email->getCreatedBy()
)
) {
return $this->accessDenied();
}
$content = $email->getCustomHtml();
$clickStats = $model->getEmailClickStats($emailId);
$totalUniqueClicks = array_sum(array_column($clickStats, 'unique_hits'));
$totalClicks = array_sum(array_column($clickStats, 'hits'));
foreach ($clickStats as &$stat) {
$stat['unique_hits_rate'] = round($totalUniqueClicks > 0 ? ($stat['unique_hits'] / $totalUniqueClicks) : 0, 4);
$stat['unique_hits_text'] = $this->translator->trans('mautic.email.heatmap.clicks', ['%count%' => $stat['unique_hits']]);
$stat['hits_rate'] = round($totalClicks > 0 ? ($stat['hits'] / $totalClicks) : 0, 4);
$stat['hits_text'] = $this->translator->trans('mautic.email.heatmap.clicks', ['%count%' => $stat['hits']]);
}
$legendTemplate = $this->renderView('@MauticEmail/Heatmap/heatmap_legend.html.twig', [
'totalClicks' => $totalClicks,
'totalUniqueClicks' => $totalUniqueClicks,
]);
return $this->sendJsonResponse([
'content' => $content,
'clickStats' => $clickStats,
'totalUniqueClicks' => $totalUniqueClicks,
'totalClicks' => $totalClicks,
'legendTemplate' => $legendTemplate,
]);
}
public function getEmailUsagesAction(Request $request, EmailDependencies $emailDependencies): JsonResponse
{
$emailId = (int) $request->query->get('id');
if (0 === $emailId) {
return $this->sendJsonResponse([
'message' => $this->translator->trans('mautic.core.error.badrequest'),
], 400);
}
$usagesHtml = $this->renderView('@MauticCore/Helper/usage.html.twig', [
'title' => $this->translator->trans('mautic.email.usages'),
'stats' => $emailDependencies->getChannelsIds($emailId),
'noUsages' => $this->translator->trans('mautic.email.no_usages'),
]);
return $this->sendJsonResponse([
'usagesHtml' => $usagesHtml,
]);
}
}

View File

@@ -0,0 +1,233 @@
<?php
namespace Mautic\EmailBundle\Controller\Api;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\Persistence\ManagerRegistry;
use Mautic\ApiBundle\Controller\CommonApiController;
use Mautic\ApiBundle\Helper\EntityResultHelper;
use Mautic\CoreBundle\Factory\ModelFactory;
use Mautic\CoreBundle\Helper\AppVersion;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\InputHelper;
use Mautic\CoreBundle\Helper\RandomHelper\RandomHelperInterface;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Helper\MailHelper;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\EmailBundle\MonitoredEmail\Processor\Reply;
use Mautic\LeadBundle\Controller\LeadAccessTrait;
use Mautic\LeadBundle\Entity\Lead;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
/**
* @extends CommonApiController<Email>
*/
class EmailApiController extends CommonApiController
{
use LeadAccessTrait;
/**
* @var EmailModel|null
*/
protected $model;
/**
* @var array<string, mixed>
*/
protected $extraGetEntitiesArguments = ['ignoreListJoin' => true];
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
{
$emailModel = $modelFactory->getModel('email');
\assert($emailModel instanceof EmailModel);
$this->model = $emailModel;
$this->entityClass = Email::class;
$this->entityNameOne = 'email';
$this->entityNameMulti = 'emails';
$this->serializerGroups = [
'emailDetails',
'categoryList',
'publishDetails',
'assetList',
'formList',
'leadListList',
];
$this->dataInputMasks = [
'customHtml' => 'html',
'dynamicContent' => [
'content' => 'html',
'filters' => [
'content' => 'html',
],
],
];
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
}
/**
* Obtains a list of emails.
*
* @return Response
*/
public function getEntitiesAction(Request $request, UserHelper $userHelper)
{
// get parent level only
$this->listFilters[] = [
'column' => 'e.variantParent',
'expr' => 'isNull',
];
return parent::getEntitiesAction($request, $userHelper);
}
/**
* Sends the email to it's assigned lists.
*
* @param int $id Email ID
*
* @return Response
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function sendAction(Request $request, $id)
{
$entity = $this->model->getEntity($id);
if (null === $entity || !$entity->isPublished()) {
return $this->notFound();
}
if (!$this->checkEntityAccess($entity)) {
return $this->accessDenied();
}
$lists = $request->request->all()['lists'] ?? [];
$limit = $request->request->get('limit', null);
$batch = $request->request->get('batch', null);
[$count, $failed] = $this->model->sendEmailToLists($entity, $lists, $limit, $batch);
$view = $this->view(
[
'success' => 1,
'sentCount' => $count,
'failedRecipients' => $failed,
],
Response::HTTP_OK
);
return $this->handleView($view);
}
/**
* Sends the email to a specific lead.
*
* @param int $id Email ID
* @param int $leadId Lead ID
*
* @return Response
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function sendLeadAction(Request $request, $id, $leadId)
{
$entity = $this->model->getEntity($id);
if (!$entity) {
return $this->notFound();
}
if (!$this->checkEntityAccess($entity)) {
return $this->accessDenied();
}
/** @var Lead $lead */
$lead = $this->checkLeadAccess($leadId, 'edit');
if ($lead instanceof Response) {
return $lead;
}
$post = $request->request->all();
$tokens = (!empty($post['tokens'])) ? $post['tokens'] : [];
$assetsIds = (!empty($post['assetAttachments'])) ? $post['assetAttachments'] : [];
$response = ['success' => false];
$cleanTokens = [];
foreach ($tokens as $token => $value) {
$value = InputHelper::html($value);
if (!preg_match('/^{.*?}$/', $token)) {
$token = '{'.$token.'}';
}
$cleanTokens[$token] = $value;
}
$leadFields = array_merge(['id' => $leadId], $lead->getProfileFields());
// Set owner_id to support the "Owner is mailer" feature
if ($lead->getOwner()) {
$leadFields['owner_id'] = $lead->getOwner()->getId();
}
$result = $this->model->sendEmail(
$entity,
$leadFields,
[
'source' => ['api', 0],
'tokens' => $cleanTokens,
'assetAttachments' => $assetsIds,
'return_errors' => true,
'ignoreDNC' => true,
'email_type' => MailHelper::EMAIL_TYPE_TRANSACTIONAL,
]
);
if (is_bool($result)) {
$response['success'] = $result;
} else {
$response['failed'] = $result;
}
$view = $this->view($response, Response::HTTP_OK);
return $this->handleView($view);
}
/**
* @param string $trackingHash
*
* @return Response
*/
public function replyAction(Reply $replyService, RandomHelperInterface $randomHelper, $trackingHash)
{
try {
$replyService->createReplyByHash($trackingHash, "api-{$randomHelper->generate()}");
} catch (EntityNotFoundException $e) {
return $this->notFound($e->getMessage());
}
return $this->handleView(
$this->view(['success' => true], Response::HTTP_CREATED)
);
}
protected function prepareParametersFromRequest(FormInterface $form, array &$params, ?object $entity = null, array $masks = [], array $fields = []): void
{
if (isset($params['publicPreview']) && $entity instanceof Email) {
$entity->setPublicPreview(InputHelper::boolean($params['publicPreview']) ?? false);
unset($params['publicPreview']);
}
parent::prepareParametersFromRequest($form, $params, $entity, $masks, $fields);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Controller;
use Mautic\CategoryBundle\Model\CategoryModel;
use Mautic\CoreBundle\Controller\AbstractFormController;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Form\Type\BatchCategoryType;
use Mautic\EmailBundle\Model\EmailActionModel;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class BatchEmailController extends AbstractFormController
{
/**
* Adds or removes categories to multiple emails defined by email ID.
*/
public function execAction(Request $request, EmailActionModel $actionModel, CategoryModel $categoryModel): JsonResponse
{
$params = $request->get('email_batch');
$ids = empty($params['ids']) ? [] : json_decode($params['ids']);
if ($ids && is_array($ids)) {
$newCategoryId = $params['newCategory'];
$newCategory = $categoryModel->getEntity($newCategoryId);
$affected = $actionModel->setCategory($ids, $newCategory);
$this->addFlashMessage('mautic.email.batch_emails_affected', [
'%count%' => count($affected),
]);
} else {
$this->addFlashMessage('mautic.core.error.ids.missing');
}
return new JsonResponse([
'closeModal' => true,
'flashes' => $this->getFlashContent(),
'affected' => !empty($affected) ? array_map(fn (Email $affected) => $affected->getId(), $affected) : [],
'newCategory' => [
'name' => !empty($newCategory) ? $newCategory->getTitle() : null,
'color' => !empty($newCategory) ? $newCategory->getColor() : null,
],
'callback' => 'emailBatchSubmitCallback',
]);
}
/**
* View the modal form for adding contacts into categories in batches.
*/
public function indexAction(): Response
{
$route = $this->generateUrl('mautic_email_batch_categories_set');
return $this->delegateView(
[
'viewParameters' => [
'form' => $this->createForm(
BatchCategoryType::class,
[],
[
'action' => $route,
'attr' => [
'data-submit-callback' => 'emailBatchSubmit',
],
]
)->createView(),
],
'contentTemplate' => '@MauticEmail/Batch/form.html.twig',
'passthroughVars' => [
'activeLink' => '#mautic_email_index',
'mauticContent' => 'emailBatch',
'route' => $route,
],
]
);
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Mautic\EmailBundle\Controller;
use Mautic\CoreBundle\Form\Type\DateRangeType;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\EmailBundle\Model\EmailModel;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class EmailGraphStatsController extends AbstractController
{
/**
* Loads a specific form into the detailed panel.
*
* @param int $objectId
* @param bool $isVariant
* @param string $dateFrom
* @param string $dateTo
*
* @throws \Exception
*/
public function viewAction(
Request $request,
EmailModel $model,
FormFactoryInterface $formFactory,
CorePermissions $security,
$objectId,
$isVariant,
$dateFrom = null,
$dateTo = null,
): \Symfony\Component\HttpFoundation\Response {
/** @var \Mautic\EmailBundle\Entity\Email $email */
$email = $model->getEntity($objectId);
// Init the date range filter form
$dateRangeValues = ['date_from' => $dateFrom, 'date_to' => $dateTo];
$action = $this->generateUrl('mautic_email_action', ['objectAction' => 'view', 'objectId' => $objectId]);
$dateRangeForm = $formFactory->create(DateRangeType::class, $dateRangeValues, ['action' => $action]);
if (null === $email || !$security->hasEntityAccess(
'email:emails:viewown',
'email:emails:viewother',
$email->getCreatedBy()
)) {
throw new AccessDeniedHttpException();
}
// get A/B test information
[$parent, $children] = $email->getVariants();
// get related translations
[$translationParent, $translationChildren] = $email->getTranslations();
// Prepare stats for bargraph
if ($chartStatsSource = $request->query->get('stats')) {
$includeVariants = ('all' === $chartStatsSource);
} else {
$includeVariants = (($email->isVariant() && $parent === $email) || ($email->isTranslation() && $translationParent === $email));
}
$dateFromObject = new \DateTime($dateFrom);
$dateToObject = new \DateTime($dateTo);
if ('template' === $email->getEmailType()) {
$stats = $model->getEmailGeneralStats(
$email,
$includeVariants,
null,
$dateFromObject,
$dateToObject
);
} else {
$stats = $model->getEmailListStats(
$email,
$includeVariants,
$dateFromObject,
$dateToObject
);
}
$statsDevices = $model->getEmailDeviceStats(
$email,
$includeVariants,
$dateFromObject,
$dateToObject
);
return $this->render(
'@MauticEmail/Email/graph.html.twig',
[
'email' => $email,
'stats' => $stats,
'statsDevices' => $statsDevices,
'showAllStats' => $includeVariants,
'dateRangeForm' => $dateRangeForm->createView(),
'isVariant' => $isVariant,
]
);
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Mautic\EmailBundle\Controller;
use Doctrine\DBAL\Exception;
use Mautic\CoreBundle\Helper\MapHelper;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\EmailBundle\Entity\Email;
use Mautic\EmailBundle\Model\EmailModel;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class EmailMapStatsController extends AbstractController
{
public const MAP_OPTIONS = [
'read_count' => [
'label' => 'mautic.email.stat.read',
'unit' => 'Read',
],
'clicked_through_count'=> [
'label' => 'mautic.email.clicked',
'unit' => 'Click',
],
];
public const LEGEND_TEXT = 'Total: %total (%withCountry with country)';
public function __construct(protected EmailModel $model)
{
}
/**
* @return array<string, array<int, array<string, int|string>>>
*
* @throws Exception
*/
public function getData(Email $entity, \DateTimeImmutable $dateFromObject, \DateTimeImmutable $dateToObject): array
{
// get A/B test information
$parent = $entity->getVariantParent();
// get translation parent
$translationParent = $entity->getTranslationParent();
$includeVariants = (($entity->isVariant() && empty($parent)) || ($entity->isTranslation() && empty($translationParent)));
return $this->model->getCountryStats(
$entity,
$dateFromObject,
$dateToObject,
$includeVariants,
);
}
public function hasAccess(CorePermissions $security, Email $entity): bool
{
return $security->hasEntityAccess(
'email:emails:viewown',
'email:emails:viewother',
$entity->getCreatedBy()
);
}
/**
* @return array<string, array<string, string>>
*/
public function getMapOptions(): array
{
return self::MAP_OPTIONS;
}
public function getMapOptionsTitle(): string
{
return 'mautic.email.stats.options.title';
}
/**
* @throws \Exception
*/
public function viewAction(
CorePermissions $security,
int $objectId,
string $dateFrom = '',
string $dateTo = '',
): Response {
$entity = $this->model->getEntity($objectId);
if (empty($entity) || !$this->hasAccess($security, $entity)) {
throw new AccessDeniedHttpException();
}
$statsCountries = $this->getData($entity, new \DateTimeImmutable($dateFrom), new \DateTimeImmutable($dateTo));
$mapData = MapHelper::buildMapData($statsCountries, $this->getMapOptions(), self::LEGEND_TEXT);
return $this->render(
'@MauticCore/Helper/map.html.twig',
[
'data' => $mapData[0]['data'],
'height' => 315,
'optionsEnabled' => true,
'optionsTitle' => $this->getMapOptionsTitle(),
'options' => $mapData,
'legendEnabled' => true,
'statUnit' => $mapData[0]['unit'],
]
);
}
}

View File

@@ -0,0 +1,814 @@
<?php
namespace Mautic\EmailBundle\Controller;
use Mautic\CoreBundle\Controller\FormController as CommonFormController;
use Mautic\CoreBundle\Helper\ThemeHelper;
use Mautic\CoreBundle\Helper\TrackingPixelHelper;
use Mautic\CoreBundle\Twig\Helper\AnalyticsHelper;
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
use Mautic\EmailBundle\EmailEvents;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Event\EmailSendEvent;
use Mautic\EmailBundle\Event\TransportWebhookEvent;
use Mautic\EmailBundle\Helper\EmailConfig;
use Mautic\EmailBundle\Helper\MailHashHelper;
use Mautic\EmailBundle\Helper\MailHelper;
use Mautic\EmailBundle\Model\EmailModel;
use Mautic\FormBundle\Model\FormModel;
use Mautic\LeadBundle\Controller\FrequencyRuleTrait;
use Mautic\LeadBundle\Entity\DoNotContact;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Helper\FakeContactHelper;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Mautic\MessengerBundle\Message\EmailHitNotification;
use Mautic\PageBundle\Entity\Page;
use Mautic\PageBundle\Event\PageDisplayEvent;
use Mautic\PageBundle\EventListener\BuilderSubscriber;
use Mautic\PageBundle\Model\PageModel;
use Mautic\PageBundle\PageEvents;
use Mautic\PluginBundle\Helper\IntegrationHelper;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class PublicController extends CommonFormController
{
use FrequencyRuleTrait;
/**
* @return Response
*/
public function indexAction(Request $request, AnalyticsHelper $analyticsHelper, $idHash)
{
/** @var EmailModel $model */
$model = $this->getModel('email');
$stat = $model->getEmailStatus($idHash);
if (!empty($stat)) {
if ($this->security->isAnonymous()) {
$model->hitEmail($stat, $request, true);
}
$tokens = $stat->getTokens();
if (is_array($tokens)) {
// Override tracking_pixel so as to not cause a double hit
$tokens['{tracking_pixel}'] = MailHelper::getBlankPixel();
}
if ($copy = $stat->getStoredCopy()) {
$subject = $copy->getSubject();
$content = $copy->getBody();
// Replace tokens
if (is_array($tokens)) {
$content = str_ireplace(array_keys($tokens), $tokens, $content);
$subject = str_ireplace(array_keys($tokens), $tokens, $subject);
}
} else {
$subject = '';
$content = '';
}
$content = $analyticsHelper->addCode($content);
// Add subject as title
if (!empty($subject)) {
if (str_contains($content, '<title></title>')) {
$content = str_replace('<title></title>', "<title>$subject</title>", $content);
} elseif (!str_contains($content, '<title>')) {
$content = str_replace('<head>', "<head>\n<title>$subject</title>", $content);
}
}
return new Response($content);
}
return $this->notFound();
}
public function trackingImageAction(
Request $request,
MessageBusInterface $messageBus,
LoggerInterface $logger,
string $idHash,
): Response {
try {
$messageBus->dispatch(new EmailHitNotification($idHash, $request));
} catch (\Exception $exception) {
$logger->error($exception->getMessage(), ['idHash' => $idHash]);
$emailModel = $this->getModel('email');
assert($emailModel instanceof EmailModel);
$emailModel->hitEmail($idHash, $request);
}
return TrackingPixelHelper::getResponse($request);
}
/**
* @return Response
*
* @throws \Exception
* @throws \Mautic\CoreBundle\Exception\FileNotFoundException
*/
public function unsubscribeAction(Request $request, ContactTracker $contactTracker, EmailModel $model, LeadModel $leadModel, FormModel $formModel, PageModel $pageModel, MailHashHelper $mailHash, ThemeHelper $themeHelper, $idHash, ?string $urlEmail = null, ?string $secretHash = null)
{
$stat = $model->getEmailStatus($idHash);
$message = '';
$email = null;
$lead = null;
$template = null;
$session = $request->getSession();
$isOneClickUnsubscribe = $request->isMethod(Request::METHOD_POST) && 'One-Click' === $request->get('List-Unsubscribe');
$isUnsubscribeAll = $request->get('unsubscribe_all');
$showContactPreferences = $this->coreParametersHelper->get('show_contact_preferences');
if (!empty($stat)) {
if ($isOneClickUnsubscribe) {
// RFC 8058 One-Click unsubscribe
$unsubscribeComment = $this->translator->trans('mautic.email.dnc.unsubscribed');
$model->setDoNotContact($stat, $unsubscribeComment, DoNotContact::UNSUBSCRIBED);
return new Response($this->translator->trans('mautic.lead.do.not.contact_unsubscribed'));
}
$email = $stat->getEmail();
}
$isCorrectHash = $secretHash && $urlEmail && $mailHash->getEmailHash($urlEmail) === $secretHash;
if ($email) {
$template = $email->getTemplate();
if ('mautic_code_mode' === $template) {
// Use system default
$template = null;
}
/** @var \Mautic\FormBundle\Entity\Form $unsubscribeForm */
$unsubscribeForm = $email->getUnsubscribeForm();
if (null != $unsubscribeForm && $unsubscribeForm->isPublished()) {
$formTemplate = $unsubscribeForm->getTemplate();
$formContent = '<div class="mautic-unsubscribeform">'.$formModel->getContent($unsubscribeForm).'</div>';
}
} else {
if ($isOneClickUnsubscribe) {
return new Response($this->translator->trans('mautic.email.stat_record.not_found'), Response::HTTP_NOT_FOUND);
}
}
if (empty($template) && empty($formTemplate)) {
$template = $this->coreParametersHelper->get('theme');
} elseif (!empty($formTemplate)) {
$template = $formTemplate;
}
$theme = $themeHelper->getTheme($template);
if ($theme->getTheme() != $template) {
$template = $theme->getTheme();
}
$contentTemplate = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/message.html.twig');
if (!empty($stat) || $isCorrectHash) {
$successSessionName = 'mautic.email.prefscenter.success';
if (!empty($stat) && $lead = $stat->getLead()) {
// Set the lead as current lead
$contactTracker->setTrackedContact($lead);
// Set lead lang
if ($language = $lead->getPreferredLocale()) {
$this->translator->setLocale($language);
}
// Add contact ID to the session name in case more contacts
// share the same session/device and the contact is known.
$successSessionName .= ".{$lead->getId()}";
} elseif (empty($stat)) {
$leadRepo = $leadModel->getRepository();
$contacts = $leadRepo->getContactsByEmail($urlEmail);
$lead = null;
if (is_array($contacts) && count($contacts) > 0) {
$lead = array_pop($contacts);
} else {
$message = $this->translator->trans('mautic.email.stat_record.not_found');
}
}
if (!$showContactPreferences || $isUnsubscribeAll) {
if (!empty($stat)) {
$message = $this->getUnsubscribeMessage($idHash, $model, $stat, $this->translator);
} elseif ($lead && $lead instanceof Lead) {
$message = $this->getUnsubscribeMessageLead($idHash, $model, $lead, $this->translator, $urlEmail);
}
} elseif ($lead) {
$params = ['idHash' => $idHash, 'urlEmail' => $urlEmail];
if ($urlEmail) {
$params['secretHash'] = $mailHash->getEmailHash($urlEmail);
}
$action = $this->generateUrl('mautic_email_unsubscribe', $params);
$viewParameters = [
'lead' => $lead,
'idHash' => $idHash,
'showContactFrequency' => $this->coreParametersHelper->get('show_contact_frequency'),
'showContactPauseDates' => $this->coreParametersHelper->get('show_contact_pause_dates'),
'showContactPreferredChannels' => $this->coreParametersHelper->get('show_contact_preferred_channels'),
'showContactCategories' => $this->coreParametersHelper->get('show_contact_categories'),
'showContactSegments' => $this->coreParametersHelper->get('show_contact_segments'),
'dncUrl' => $this->generateUrl('mautic_email_unsubscribe_all', $params),
];
if ($session->get($successSessionName)) {
$viewParameters['successMessage'] = $this->translator->trans('mautic.email.preferences_center_success_message.text');
}
$form = $this->getFrequencyRuleForm($lead, $viewParameters, $data, true, $action, true);
if (true === $form) {
$session->set($successSessionName, 1);
return $this->postActionRedirect(
[
'returnUrl' => $action,
'viewParameters' => $viewParameters,
'contentTemplate' => $contentTemplate,
]
);
} else {
// success message should not persist on page refresh
$session->set($successSessionName, 0);
}
$formView = $form->createView();
/** @var Page $prefCenter */
if ($email && ($prefCenter = $email->getPreferenceCenter()) && $prefCenter->getIsPreferenceCenter()) {
// Set the page language if there is no lead preferred locale
if (empty($language) && $language = $prefCenter->getLanguage()) {
$this->translator->setLocale($language);
}
$html = $prefCenter->getCustomHtml();
// check if tokens are present
if (str_contains($html, BuilderSubscriber::saveprefsRegex)) {
// set custom tag to inject end form
// update show pref center tokens by looking for their presence in the html
$showParameters = $this->buildShowParametersBasedOnContent($html, $viewParameters);
$eventParameters = array_merge(
$viewParameters,
$showParameters,
[
'form' => $formView,
'startform' => $this->renderView('@MauticCore/Default/form.html.twig', ['form' => $formView]),
'custom_tag' => '<a name="end-'.$formView->vars['id'].'"></a>',
]
);
$event = new PageDisplayEvent($html, $prefCenter, $eventParameters);
$this->dispatcher->dispatch($event, PageEvents::PAGE_ON_DISPLAY);
$html = $event->getContent();
if (!$session->has($successSessionName)) {
$successMessageData = ['class="pref-successmessage"'];
$successMessageDataHidden = [];
foreach ($successMessageData as $successMessageData) {
$successMessageDataHidden[] = $successMessageData.' style=display:none';
}
$html = str_replace(
$successMessageData,
$successMessageDataHidden,
$html
);
} else {
$session->remove($successSessionName);
}
$html = preg_replace(
'/'.BuilderSubscriber::identifierToken.'/',
$lead->getPrimaryIdentifier(),
$html
);
$pageModel->hitPage($prefCenter, $request, 200, $lead);
} else {
unset($html);
}
}
if (empty($html)) {
$html = $this->render(
'@MauticEmail/Lead/preference_options.html.twig',
array_merge(
$viewParameters,
[
'form' => $formView,
'currentRoute' => $this->generateUrl(
'mautic_contact_action',
[
'objectAction' => 'contactFrequency',
'objectId' => $lead->getId(),
]
),
]
)
)->getContent();
}
$message = $html;
}
} else {
$message = $this->translator->trans('mautic.email.stat_record.not_found');
}
$config = $theme->getConfig();
$viewParams = [
'email' => $email,
'lead' => $lead,
'template' => $template,
'message' => $message,
];
if (!empty($formContent)) {
$viewParams['content'] = $formContent;
if (in_array('form', $config['features'])) {
$contentTemplate = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/form.html.twig');
} else {
$viewParams['content'] = '';
$viewParams['message'] = $message.$formContent;
}
}
return $this->render($contentTemplate, $viewParams);
}
public function unsubscribeAllAction(Request $request, string $idHash, ?string $urlEmail = null, ?string $secretHash = null): Response
{
$request->attributes->set('unsubscribe_all', 1);
return $this->forward(static::class.'::unsubscribeAction', [
'request' => $request,
'idHash' => $idHash,
'urlEmail' => $urlEmail,
'secretHash' => $secretHash,
]);
}
/**
* @throws \Exception
* @throws \Mautic\CoreBundle\Exception\FileNotFoundException
*/
public function resubscribeAction(ContactTracker $contactTracker, EmailModel $model, MailHashHelper $mailHash, ThemeHelper $themeHelper, AssetsHelper $assetsHelper, AnalyticsHelper $analyticsHelper, $idHash): Response
{
$stat = $model->getEmailStatus($idHash);
if (!empty($stat)) {
$email = $stat->getEmail();
$lead = $stat->getLead();
if ($lead) {
// Set the lead as current lead
$contactTracker->setTrackedContact($lead);
if (!$this->translator instanceof LocaleAwareInterface) {
throw new \LogicException(sprintf('$this->translator must be an instance of "%s"', LocaleAwareInterface::class));
}
// Set lead lang
if ($lead->getPreferredLocale()) {
$this->translator->setLocale($lead->getPreferredLocale());
}
}
$model->removeDoNotContact($stat->getEmailAddress());
$message = $this->coreParametersHelper->get('resubscribe_message');
$toEmail = $stat->getEmailAddress();
$unsubscribeHash = $mailHash->getEmailHash($toEmail);
if (!$message) {
$message = $this->translator->trans(
'mautic.email.resubscribed.success',
[
'%unsubscribeUrl%' => '|URL|',
'%email%' => '|EMAIL|',
]
);
}
$message = str_replace(
[
'|URL|',
'|EMAIL|',
],
[
$this->generateUrl('mautic_email_unsubscribe', ['idHash' => $idHash, 'urlEmail' => $toEmail, 'secretHash' => $unsubscribeHash]),
$stat->getEmailAddress(),
],
$message
);
} else {
$email = $lead = false;
$message = $this->translator->trans('mautic.email.stat_record.not_found');
}
$template = (!empty($email) && 'mautic_code_mode' !== $email->getTemplate()) ? $email->getTemplate() : $this->coreParametersHelper->get('theme');
$theme = $themeHelper->getTheme($template);
if ($theme->getTheme() != $template) {
$template = $theme->getTheme();
}
// Ensure template still exists
$theme = $themeHelper->getTheme($template);
if (empty($theme) || $theme->getTheme() !== $template) {
$template = $this->coreParametersHelper->get('theme');
}
$analytics = $analyticsHelper->getCode();
if (!empty($analytics)) {
$assetsHelper->addCustomDeclaration($analytics);
}
$logicalName = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/message.html.twig');
return $this->render(
$logicalName,
[
'message' => $message,
'type' => 'notice',
'email' => $email,
'lead' => $lead,
'template' => $template,
]
);
}
/**
* Handles mailer transport webhook post.
*/
public function mailerCallbackAction(Request $request): Response
{
$event = new TransportWebhookEvent($request);
$this->dispatcher->dispatch($event, EmailEvents::ON_TRANSPORT_WEBHOOK);
return $event->getResponse() ?? new Response('No email transport that could process this callback was found', Response::HTTP_NOT_FOUND);
}
/**
* Preview email.
*
* @return Response
*/
public function previewAction(
AnalyticsHelper $analyticsHelper,
ThemeHelper $themeHelper,
AssetsHelper $assetsHelper,
EmailConfig $emailConfig,
EmailModel $model,
Request $request,
LeadModel $leadModel,
FakeContactHelper $fakeLeadHelper,
string $objectId,
?string $objectType = null,
) {
$contactId = (int) $request->query->get('contactId');
$emailEntity = $model->getEntity($objectId);
if (null === $emailEntity) {
return $this->notFound();
}
$publicPreview = $emailEntity->isPublicPreview();
$draftEnabled = $emailConfig->isDraftEnabled();
if ('draft' === $objectType && $draftEnabled && $emailEntity->hasDraft()) {
$publicPreview = $emailEntity->getDraft()->isPublicPreview();
}
if (
($this->security->isAnonymous() && !$publicPreview)
|| (!$this->security->isAnonymous()
&& !$this->security->hasEntityAccess(
'email:emails:viewown',
'email:emails:viewother',
$emailEntity->getCreatedBy()
))
) {
return $this->accessDenied();
}
// bogus ID
if ($contactId && (
!$this->security->isAdmin()
&& !$this->security->hasEntityAccess('lead:leads:viewown', 'lead:leads:viewother')
)
) {
// disallow displaying contact information
$contactId = null;
}
// bogus ID
$idHash = 'xxxxxxxxxxxxxx';
$BCcontent = $emailEntity->getContent();
$content = $emailEntity->getCustomHtml();
if ('draft' === $objectType && $draftEnabled && $emailEntity->hasDraft()) {
$content = $emailEntity->getDraftContent();
}
if (empty($content) && !empty($BCcontent)) {
$template = $emailEntity->getTemplate();
$assetsHelper->addCustomDeclaration('<meta name="robots" content="noindex">');
$logicalName = $themeHelper->checkForTwigTemplate('@themes/'.$template.'/html/email.html.twig');
$response = $this->render(
$logicalName,
[
'inBrowser' => true,
'content' => $emailEntity->getContent(),
'email' => $emailEntity,
'lead' => null,
'template' => $template,
]
);
// replace tokens
$content = $response->getContent();
}
// Override tracking_pixel
$tokens = ['{tracking_pixel}' => ''];
// Prepare contact
if ($contactId) {
// We have one from request parameter
/** @var LeadModel $leadModel */
$contact = $leadModel->getRepository()->getLead($contactId);
$contact = $model->enrichedContactWithCompanies($contact);
} else {
// Make fake contact.
/** @var FakeContactHelper $fakeLeadHelper */
$contact = $fakeLeadHelper->prepareFakeContactWithPrimaryCompany();
}
// Generate and replace tokens
$event = new EmailSendEvent(
null,
[
'content' => $content,
'email' => $emailEntity,
'idHash' => $idHash,
'tokens' => $tokens,
'internalSend' => true,
'lead' => $contact,
]
);
$this->dispatcher->dispatch($event, EmailEvents::EMAIL_ON_DISPLAY);
$content = $event->getContent(true);
if ($this->security->isAnonymous()) {
$content = $analyticsHelper->addCode($content);
}
return new Response($content);
}
/**
* @throws \Exception
*/
private function doTracking(Request $request, IntegrationHelper $integrationHelper, MailHelper $mailer, LoggerInterface $mauticLogger, $integration): void
{
$logger = $mauticLogger;
// if additional data were sent with the tracking pixel
$query_string = $request->server->get('QUERY_STRING');
if (!$query_string) {
$logger->log('error', $integration.': query string is not available');
return;
}
if (str_starts_with($query_string, 'r=')) {
$query_string = substr($query_string, strpos($query_string, '?') + 1);
} // remove route variable
parse_str($query_string, $query);
// URL attr 'd' is encoded so let's decode it first.
if (!isset($query['d'], $query['sig'])) {
$logger->log('error', $integration.': query variables are not found');
return;
}
// get secret from plugin settings
$myIntegration = $integrationHelper->getIntegrationObject($integration);
if (!$myIntegration) {
$logger->log('error', $integration.': integration not found');
return;
}
$keys = $myIntegration->getDecryptedApiKeys();
// generate signature
$salt = $keys['secret'];
if (!str_contains($salt, '$1$')) {
$salt = '$1$'.$salt;
} // add MD5 prefix
$cr = crypt(urlencode($query['d']), $salt);
$mySig = hash('crc32b', $cr); // this hash type is used in c#
// compare signatures
if (hash_equals($mySig, $query['sig'])) {
// decode and parse query variables
$b64 = base64_decode($query['d']);
$gz = gzdecode($b64);
parse_str($gz, $query);
} else {
// signatures don't match: stop
$logger->log('error', $integration.': signatures don\'t match');
unset($query);
}
if (empty($query) || !isset($query['email'], $query['subject'], $query['body'])) {
$logger->log('error', $integration.': query variables are empty');
return;
}
if (MAUTIC_ENV === 'dev') {
$logger->log('error', $integration.': '.json_encode($query, JSON_PRETTY_PRINT));
}
/** @var EmailModel $model */
$model = $this->getModel('email');
// email is a semicolon delimited list of emails
$emails = explode(';', $query['email']);
$leadModel = $this->getModel('lead');
\assert($leadModel instanceof LeadModel);
$repo = $leadModel->getRepository();
foreach ($emails as $email) {
$lead = $repo->getLeadByEmail($email);
if (null === $lead) {
$lead = $this->createLead($email, $repo);
if (null === $lead) {
continue;
}
}
$idHash = hash('crc32', $email.$query['body']);
$idHash = substr($idHash.$idHash, 0, 13); // 13 bytes length
$stat = $model->getEmailStatus($idHash);
// stat doesn't exist, create one
if (null === $stat) {
$lead['email'] = $email; // needed for stat
$stat = $this->addStat($mailer, $lead, $email, $query, $idHash);
}
$stat->setSource('email.client');
if ($stat || 'Outlook' !== $integration) { // Outlook requests the tracking gif on send
$model->hitEmail($idHash, $request); // add email event
}
}
}
/**
* @return Response
*/
public function pluginTrackingGifAction(Request $request, IntegrationHelper $integrationHelper, MailHelper $mailer, LoggerInterface $mauticLogger, $integration)
{
$this->doTracking($request, $integrationHelper, $mailer, $mauticLogger, $integration);
return TrackingPixelHelper::getResponse($request); // send gif
}
private function addStat(MailHelper $mailer, $lead, $email, $query, $idHash): ?Stat
{
if (null !== $lead) {
// To lead
$mailer->addTo($email);
// sanitize variables to prevent malicious content
$from = filter_var($query['from'], FILTER_SANITIZE_EMAIL);
$mailer->setFrom($from, '');
// Set Content
$body = htmlspecialchars(filter_var($query['body'], FILTER_FLAG_STRIP_HIGH));
$mailer->setBody($body);
$mailer->parsePlainText($body);
// Set lead
$mailer->setLead($lead);
$mailer->setIdHash($idHash);
$subject = htmlspecialchars(filter_var($query['subject'], FILTER_FLAG_STRIP_HIGH));
$mailer->setSubject($subject);
return $mailer->createEmailStat();
}
return null;
}
private function createLead($email, $repo): ?Lead
{
$model = $this->getModel('lead.lead');
\assert($model instanceof LeadModel);
$lead = $model->getEntity();
// set custom field values
$data = ['email' => $email];
$model->setFieldValues($lead, $data, true);
// create lead
$model->saveEntity($lead);
// return entity
return $repo->getLeadByEmail($email);
}
public function getUnsubscribeMessage($idHash, $model, $stat, $translator): string
{
$model->setDoNotContact($stat, $translator->trans('mautic.email.dnc.unsubscribed'), DoNotContact::UNSUBSCRIBED);
return $this->getUnsubscribeText($translator, $stat->getEmailAddress(), $idHash);
}
public function getUnsubscribeMessageLead(string $idHash, EmailModel $model, Lead $lead, TranslatorInterface $translator, string $urlEmail): string
{
$model->setDoNotContactLead($lead, $translator->trans('mautic.email.dnc.unsubscribed'), DoNotContact::UNSUBSCRIBED);
return $this->getUnsubscribeText($translator, $urlEmail, $idHash);
}
private function getUnsubscribeText(TranslatorInterface $translator, string $email, string $idHash): string
{
$message = $this->coreParametersHelper->get('unsubscribe_message');
if (!$message) {
$message = $translator->trans(
'mautic.email.unsubscribed.success',
[
'%resubscribeUrl%' => '|URL|',
'%email%' => '|EMAIL|',
]
);
}
return str_replace(
[
'|URL|',
'|EMAIL|',
],
[
$this->generateUrl('mautic_email_resubscribe', ['idHash' => $idHash]),
$email,
],
$message
);
}
/**
* The $viewParameters here have already been used to build the $form.
* Fields that are set to show based on the app configuration are part
* of the form. If the field is not configured to show, but a token exists
* for that field in the content, then we need to keep the configuration
* value instead of letting the content determine if it should show. This
* is because of what was stated above - fields that are not configured to
* to show are not part of the form. Attempting to render them will result
* in an error.
*
* @param mixed[] $viewParameters
*
* @return mixed[]
*/
private function buildShowParametersBasedOnContent(string $content, array $viewParameters): array
{
/*
* Since we're going to be merging this with the $viewParameters, filter out `true` values. We do not
* want to change a configured value from `false` to `true` because a value of `false` in the $viewParameters
* means that the field is not configured to show and therefore is not part of the form. Attempting to
* render that field just because a token for it exists will result in an error.
*/
$showParamsBasedOnContent = array_filter([
'showContactFrequency' => str_contains($content, BuilderSubscriber::channelfrequency),
'showContactSegments' => str_contains($content, BuilderSubscriber::segmentListRegex),
'showContactCategories' => str_contains($content, BuilderSubscriber::categoryListRegex),
'showContactPreferredChannels' => str_contains($content, BuilderSubscriber::preferredchannel),
], fn (bool $value) =>!$value);
$showParamsBasedOnConfiguration = array_filter($viewParameters, fn ($key) => str_starts_with($key, 'show'), ARRAY_FILTER_USE_KEY);
return array_merge($showParamsBasedOnConfiguration, $showParamsBasedOnContent);
}
}