diff --git a/Config/config.php b/Config/config.php
index 8cb8d4f..8216c37 100644
--- a/Config/config.php
+++ b/Config/config.php
@@ -7,4 +7,7 @@
'description' => 'Sparkpost Mailer Plugin for Mautic',
'version' => '1.0.0',
'author' => 'Acquia',
+ 'parameters' => [
+ 'sparkpost_tracking_enabled' => false,
+ ],
];
diff --git a/Mailer/Factory/SparkpostTransportFactory.php b/Mailer/Factory/SparkpostTransportFactory.php
index b364676..73ceb94 100644
--- a/Mailer/Factory/SparkpostTransportFactory.php
+++ b/Mailer/Factory/SparkpostTransportFactory.php
@@ -4,6 +4,7 @@
namespace MauticPlugin\SparkpostBundle\Mailer\Factory;
+use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\EmailBundle\Model\TransportCallback;
use MauticPlugin\SparkpostBundle\Mailer\Transport\SparkpostTransport;
use Psr\Log\LoggerInterface;
@@ -21,6 +22,7 @@ class SparkpostTransportFactory extends AbstractTransportFactory
public function __construct(
private TransportCallback $transportCallback,
private TranslatorInterface $translator,
+ private CoreParametersHelper $coreParametersHelper,
EventDispatcherInterface $eventDispatcher,
HttpClientInterface $client = null,
LoggerInterface $logger = null,
@@ -51,9 +53,10 @@ public function create(Dsn $dsn): TransportInterface
$this->getPassword($dsn),
$region,
$this->transportCallback,
+ $this->coreParametersHelper,
$this->client,
$this->dispatcher,
- $this->logger
+ $this->logger,
);
}
diff --git a/Mailer/Transport/SparkpostTransport.php b/Mailer/Transport/SparkpostTransport.php
index ebeda22..4cacaf6 100644
--- a/Mailer/Transport/SparkpostTransport.php
+++ b/Mailer/Transport/SparkpostTransport.php
@@ -4,6 +4,7 @@
namespace MauticPlugin\SparkpostBundle\Mailer\Transport;
+use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\EmailBundle\Helper\MailHelper;
use Mautic\EmailBundle\Mailer\Message\MauticMessage;
use Mautic\EmailBundle\Mailer\Transport\TokenTransportInterface;
@@ -23,7 +24,7 @@
use Symfony\Component\Mime\Header\ParameterizedHeader;
use Symfony\Component\Mime\Header\UnstructuredHeader;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
-use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
@@ -56,6 +57,7 @@ public function __construct(
private string $apiKey,
string $region,
private TransportCallback $callback,
+ private CoreParametersHelper $coreParametersHelper,
HttpClientInterface $client = null,
EventDispatcherInterface $dispatcher = null,
LoggerInterface $logger = null,
@@ -70,10 +72,6 @@ public function __toString(): string
}
/**
- * @throws ClientExceptionInterface
- * @throws DecodingExceptionInterface
- * @throws RedirectionExceptionInterface
- * @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
@@ -92,8 +90,8 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
}
return $response;
- } catch (\Exception $e) {
- throw new TransportException($e->getMessage());
+ } catch (ExceptionInterface $e) {
+ throw new TransportException($e->getMessage(), 0, $e);
}
}
@@ -131,6 +129,8 @@ private function getSparkpostPayload(SentMessage $message): array
}
}
+ $trackingEnabled = (bool) $this->coreParametersHelper->get('sparkpost_tracking_enabled', false);
+
return [
'content' => $this->buildContent($email),
'recipients' => $this->buildRecipients($email, $metadata, $mergeVars),
@@ -142,8 +142,8 @@ private function getSparkpostPayload(SentMessage $message): array
: [],
'campaign_id' => $this->getCampaignId($metadata, $metadataSet),
'options' => [
- 'open_tracking' => false,
- 'click_tracking' => false,
+ 'open_tracking' => $trackingEnabled,
+ 'click_tracking' => $trackingEnabled,
],
];
}
diff --git a/README.md b/README.md
index dcebbbf..f78adb5 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,46 @@
-### Mautic Sparkpost Plugin
+# Mautic Sparkpost Plugin
This plugin enable Mautic 5 to run Sparkpost as an email transport. Features:
- API transport. This transport can send up to 2000 emails per API request which makes it very fast compared to SMTP.
- Bounce webhook handling. This plugin will unsubscribe contacts in Mautic based on the hard bounces while Sparkpost will take care of the soft bounce retrieals.
+## Installation
-#### Mautic Mailer DSN Scheme
+There are several ways how to install this plugin. Here are the options from best to worst.
+
+### Via Composer
+
+This is the best option for Mautic instances that were installed via Composer (recommended way to install Mautic)
+
+Steps:
+1. `composer install acquia/mc-cs-plugin-sparkpost`
+2. `bin/console mautic:plugins:install`
+
+### Via Git
+
+This option is useful for development or testing of this plugin as you'll be able to checkout different branches of this repository.
+
+Steps:
+1. `cd plugins`
+2. `git clone git@github.com:acquia/mc-cs-plugin-sparkpost.git SparkpostBundle`
+3. `cd ..`
+4. `bin/console mautic:plugins:install`
+
+### Via SFTP
+
+You should reconsider using this method as the other two above are way better, but this is also possible.
+
+Steps:
+1. [Download this plugin](https://github.com/acquia/mc-cs-plugin-sparkpost/archive/refs/heads/main.zip)
+2. Rename the folder `mc-cs-plugin-sparkpost-main` to `SparkpostBundle`
+3. Upload this folder to the `plugins` directory of your Mautic files.
+4. `bin/console mautic:plugins:install`
+
+## Configuration
+
+After the plugin is installed go to the Mautic's global configuration, the Email settings and configure the DSN.
+
+### Mautic Mailer DSN Scheme
`mautic+sparkpost+api`
#### Mautic Mailer DSN Example
@@ -16,7 +51,15 @@ This plugin enable Mautic 5 to run Sparkpost as an email transport. Features:
-### Testing
+### Sparkpost tracking
+
+The Sparkpost tracking is disabled by default as then the email open and clicks would be tracked twice. Once by Sparkpost, second time by Mautic. This can create some unexpected behavior. The Sparkpost tracking is disabled by default, but you can enable it by adding this row to the Mautic configuration file located at `config/local.php`:
+
+```php
+'sparkpost_tracking_enabled' => true,
+```
+
+## Testing
To run all tests `composer phpunit`
diff --git a/Tests/Functional/Mailer/Transport/SparkpostTransportTest.php b/Tests/Functional/Mailer/Transport/SparkpostTransportTest.php
index c3767b9..7add0b9 100644
--- a/Tests/Functional/Mailer/Transport/SparkpostTransportTest.php
+++ b/Tests/Functional/Mailer/Transport/SparkpostTransportTest.php
@@ -21,29 +21,37 @@ class SparkpostTransportTest extends MauticMysqlTestCase
protected function setUp(): void
{
- $this->configParams['mailer_dsn'] = 'mautic+sparkpost+api://:some_api@some_host:25?region=us';
- $this->configParams['messenger_dsn_email'] = 'sync://';
- $this->configParams['mailer_custom_headers'] = ['x-global-custom-header' => 'value123'];
- $this->configParams['mailer_from_email'] = 'admin@mautic.test';
- $this->configParams['mailer_from_name'] = 'Admin';
+ $this->configParams['mailer_dsn'] = 'mautic+sparkpost+api://:some_api@some_host:25?region=us';
+ $this->configParams['messenger_dsn_email'] = 'sync://';
+ $this->configParams['mailer_custom_headers'] = ['x-global-custom-header' => 'value123'];
+ $this->configParams['mailer_from_email'] = 'admin@mautic.test';
+ $this->configParams['mailer_from_name'] = 'Admin';
+ $this->configParams['sparkpost_tracking_enabled'] = 'testEmailSendToContactSync' === $this->getName(false) ? $this->getProvidedData()[0] : false;
parent::setUp();
$this->translator = self::getContainer()->get('translator');
}
- public function testEmailSendToContactSync(): void
+ /**
+ * @dataProvider provideTrackingConfig
+ */
+ public function testEmailSendToContactSync(bool $expectedTrackingConfig): void
{
$expectedResponses = [
- function ($method, $url, $options): MockResponse {
+ function ($method, $url, $options) use ($expectedTrackingConfig): MockResponse {
Assert::assertSame(Request::METHOD_POST, $method);
Assert::assertSame('https://api.sparkpost.com/api/v1/utils/content-previewer/', $url);
- $this->assertSparkpostRequestBody($options['body']);
+ $bodyArray = json_decode($options['body'], true);
+ $this->assertSparkpostRequestBody($bodyArray, $expectedTrackingConfig);
+ $this->assertSubstitutionData($bodyArray['substitution_data']);
return new MockResponse('{"results": {"subject": "Hello there!", "html": "This is test body for {contactfield=email}!"}}');
},
- function ($method, $url, $options): MockResponse {
+ function ($method, $url, $options) use ($expectedTrackingConfig): MockResponse {
Assert::assertSame(Request::METHOD_POST, $method);
Assert::assertSame('https://api.sparkpost.com/api/v1/transmissions/', $url);
- $this->assertSparkpostRequestBody($options['body']);
+ $bodyArray = json_decode($options['body'], true);
+ $this->assertSparkpostRequestBody($bodyArray, $expectedTrackingConfig);
+ $this->assertSubstitutionData($bodyArray['recipients'][0]['substitution_data']);
return new MockResponse('{"results": {"total_rejected_recipients": 0, "total_accepted_recipients": 1, "id": "11668787484950529"}}');
},
@@ -95,6 +103,15 @@ function ($method, $url, $options): MockResponse {
Assert::assertSame('', $email->getReplyTo()[0]->getName());
}
+ /**
+ * @return array
+ */
+ public function provideTrackingConfig(): iterable
+ {
+ yield 'sparkpost_tracking_enabled is TRUE' => [true];
+ yield 'sparkpost_tracking_enabled is FALSE' => [false];
+ }
+
public function testTestTransportButton(): void
{
$expectedResponses = [
@@ -132,24 +149,33 @@ private function assertSparkpostTestRequestBody(string $body): void
Assert::assertSame('Hi! This is a test email from Mautic. Testing...testing...1...2...3!', $bodyArray['content']['text']);
}
- private function assertSparkpostRequestBody(string $body): void
+ /**
+ * @param mixed[] $bodyArray
+ */
+ private function assertSparkpostRequestBody(array $bodyArray, bool $expectedTrackingConfig): void
{
- $bodyArray = json_decode($body, true);
Assert::assertSame('Admin User ', $bodyArray['content']['from']);
Assert::assertSame('value123', $bodyArray['content']['headers']['x-global-custom-header']);
Assert::assertSame('This is test body for {{{ CONTACTFIELDEMAIL }}}!
', $bodyArray['content']['html']);
- Assert::assertSame('admin@mautic.test', $bodyArray['content']['reply_to']);
+ Assert::assertSame('admin@yoursite.com', $bodyArray['content']['reply_to']);
Assert::assertSame('Hello there!', $bodyArray['content']['subject']);
Assert::assertSame('This is test body for {{{ CONTACTFIELDEMAIL }}}!', $bodyArray['content']['text']);
- Assert::assertSame(['open_tracking' => false, 'click_tracking' => false], $bodyArray['options']);
- Assert::assertSame('contact@an.email', $bodyArray['substitution_data']['CONTACTFIELDEMAIL']);
- Assert::assertSame('Hello there!', $bodyArray['substitution_data']['SUBJECT']);
- Assert::assertArrayHasKey('SIGNATURE', $bodyArray['substitution_data']);
- Assert::assertArrayHasKey('TRACKINGPIXEL', $bodyArray['substitution_data']);
- Assert::assertArrayHasKey('UNSUBSCRIBETEXT', $bodyArray['substitution_data']);
- Assert::assertArrayHasKey('UNSUBSCRIBEURL', $bodyArray['substitution_data']);
- Assert::assertArrayHasKey('WEBVIEWTEXT', $bodyArray['substitution_data']);
- Assert::assertArrayHasKey('WEBVIEWURL', $bodyArray['substitution_data']);
+ Assert::assertSame(['open_tracking' => $expectedTrackingConfig, 'click_tracking' => $expectedTrackingConfig], $bodyArray['options']);
+ }
+
+ /**
+ * @param array $substitutionData
+ */
+ private function assertSubstitutionData(array $substitutionData): void
+ {
+ Assert::assertSame('contact@an.email', $substitutionData['CONTACTFIELDEMAIL']);
+ Assert::assertSame('Hello there!', $substitutionData['SUBJECT']);
+ Assert::assertArrayHasKey('SIGNATURE', $substitutionData);
+ Assert::assertArrayHasKey('TRACKINGPIXEL', $substitutionData);
+ Assert::assertArrayHasKey('UNSUBSCRIBETEXT', $substitutionData);
+ Assert::assertArrayHasKey('UNSUBSCRIBEURL', $substitutionData);
+ Assert::assertArrayHasKey('WEBVIEWTEXT', $substitutionData);
+ Assert::assertArrayHasKey('WEBVIEWURL', $substitutionData);
}
private function createContact(string $email): Lead
diff --git a/Tests/Unit/Mailer/Factory/SparkpostTransportFactoryTest.php b/Tests/Unit/Mailer/Factory/SparkpostTransportFactoryTest.php
index eec7a73..f313b5f 100644
--- a/Tests/Unit/Mailer/Factory/SparkpostTransportFactoryTest.php
+++ b/Tests/Unit/Mailer/Factory/SparkpostTransportFactoryTest.php
@@ -4,6 +4,7 @@
namespace MauticPlugin\SparkpostBundle\Tests\Unit\Mailer\Factory;
+use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\EmailBundle\Model\TransportCallback;
use MauticPlugin\SparkpostBundle\Mailer\Factory\SparkpostTransportFactory;
use MauticPlugin\SparkpostBundle\Mailer\Transport\SparkpostTransport;
@@ -22,19 +23,29 @@ class SparkpostTransportFactoryTest extends TestCase
{
private SparkpostTransportFactory $sparkpostTransportFactory;
- private TranslatorInterface|MockObject $translatorMock;
+ /**
+ * @var TranslatorInterface&MockObject
+ */
+ private MockObject $translatorMock;
+
+ /**
+ * @var CoreParametersHelper&MockObject
+ */
+ private MockObject $coreParametersHelper;
protected function setUp(): void
{
- $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
- $this->translatorMock = $this->createMock(TranslatorInterface::class);
- $transportCallbackMock = $this->createMock(TransportCallback::class);
- $httpClientMock = $this->createMock(HttpClientInterface::class);
- $loggerMock = $this->createMock(LoggerInterface::class);
+ $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
+ $this->translatorMock = $this->createMock(TranslatorInterface::class);
+ $transportCallbackMock = $this->createMock(TransportCallback::class);
+ $this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
+ $httpClientMock = $this->createMock(HttpClientInterface::class);
+ $loggerMock = $this->createMock(LoggerInterface::class);
$this->sparkpostTransportFactory = new SparkpostTransportFactory(
$transportCallbackMock,
$this->translatorMock,
+ $this->coreParametersHelper,
$eventDispatcherMock,
$httpClientMock,
$loggerMock
diff --git a/Tests/Unit/Mailer/Transport/SparkpostTransportMessageTest.php b/Tests/Unit/Mailer/Transport/SparkpostTransportMessageTest.php
index cc9cf87..e0bcafd 100644
--- a/Tests/Unit/Mailer/Transport/SparkpostTransportMessageTest.php
+++ b/Tests/Unit/Mailer/Transport/SparkpostTransportMessageTest.php
@@ -4,6 +4,7 @@
namespace MauticPlugin\SparkpostBundle\Tests\Unit\Mailer\Transport;
+use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\EmailBundle\Mailer\Message\MauticMessage;
use Mautic\EmailBundle\Model\TransportCallback;
use MauticPlugin\SparkpostBundle\Mailer\Transport\SparkpostTransport;
@@ -27,6 +28,7 @@ public function testCcAndBccFields(): void
$transportCallbackMock = $this->createMock(TransportCallback::class);
$httpClientMock = $this->createMock(HttpClientInterface::class);
+ $coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
$loggerMock = $this->createMock(LoggerInterface::class);
@@ -34,6 +36,7 @@ public function testCcAndBccFields(): void
'1234',
'us',
$transportCallbackMock,
+ $coreParametersHelper,
$httpClientMock,
$eventDispatcherMock,
$loggerMock
diff --git a/Tests/Unit/Mailer/Transport/SparkpostTransportTest.php b/Tests/Unit/Mailer/Transport/SparkpostTransportTest.php
index d0adedd..c92c500 100644
--- a/Tests/Unit/Mailer/Transport/SparkpostTransportTest.php
+++ b/Tests/Unit/Mailer/Transport/SparkpostTransportTest.php
@@ -4,6 +4,7 @@
namespace MauticPlugin\SparkpostBundle\Tests\Unit\Mailer\Transport;
+use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\EmailBundle\Mailer\Message\MauticMessage;
use Mautic\EmailBundle\Model\TransportCallback;
use MauticPlugin\SparkpostBundle\Mailer\Transport\SparkpostTransport;
@@ -21,19 +22,37 @@
class SparkpostTransportTest extends TestCase
{
- private TransportCallback|MockObject $transportCallbackMock;
+ /**
+ * @var TransportCallback&MockObject
+ */
+ private MockObject $transportCallbackMock;
- private HttpClientInterface|MockObject $httpClientMock;
+ /**
+ * @var CoreParametersHelper&MockObject
+ */
+ private MockObject $coreParametersHelper;
- private EventDispatcherInterface|MockObject $eventDispatcherMock;
+ /**
+ * @var HttpClientInterface&MockObject
+ */
+ private MockObject $httpClientMock;
- private LoggerInterface|MockObject $loggerMock;
+ /**
+ * @var EventDispatcherInterface&MockObject
+ */
+ private MockObject $eventDispatcherMock;
+
+ /**
+ * @var LoggerInterface&MockObject
+ */
+ private MockObject $loggerMock;
private SparkpostTransport $transport;
protected function setUp(): void
{
$this->transportCallbackMock = $this->createMock(TransportCallback::class);
+ $this->coreParametersHelper = $this->createMock(CoreParametersHelper::class);
$this->httpClientMock = $this->createMock(HttpClientInterface::class);
$this->eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
$this->loggerMock = $this->createMock(LoggerInterface::class);
@@ -41,6 +60,7 @@ protected function setUp(): void
'api-key',
'some-region',
$this->transportCallbackMock,
+ $this->coreParametersHelper,
$this->httpClientMock,
$this->eventDispatcherMock,
$this->loggerMock