hampel / sparkpost-transport
A Symfony Mailer transport for SparkPost, built on hampel/sparkpost
Requires
- php: >=8.3
- hampel/sparkpost: ^0.1.0
- symfony/mailer: ^5.4|^6.4|^7.0
- symfony/mime: ^5.4|^6.4|^7.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.8|^8.0
- hampel/rig: ^0.1.2
- laravel/pint: ^1.30
- phpstan/phpstan: ^2.1.22
- phpunit/phpunit: ^12.0
This package is auto-updated.
Last update: 2026-08-21 15:07:15 UTC
README
Sends Symfony Mailer messages through the SparkPost transmissions API, using
hampel/sparkpost to do the talking.
By Simon Hampel
Installation
composer require hampel/sparkpost-transport
You also need a PSR-18 HTTP client and a PSR-17 factory, because the API client takes whatever your application already has. Guzzle 7 provides both:
composer require guzzlehttp/guzzle
Usage
use GuzzleHttp\Client; use GuzzleHttp\Psr7\HttpFactory; use Hampel\SparkPost\Config; use Hampel\SparkPost\SparkPost; use Hampel\SparkPost\Transport\SparkPostTransport; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mime\Email; $factory = new HttpFactory(); $sparkpost = new SparkPost(new Config('MY-API-KEY'), new Client(), $factory, $factory); $transport = new SparkPostTransport($sparkpost); $mailer = new Mailer($transport); $mailer->send( (new Email()) ->from('webmaster@example.com') ->to('alice@example.com') ->subject('Hello') ->text('Hello from SparkPost.') );
HTTP 200 is not a successful send
SparkPost answers 200 having accepted zero recipients — a suppressed address, an invalid
one. A transport that reads only the status code reports that as a successful send. Here it
is a failure:
- Nobody accepted →
TransportException. Nothing was sent, and you will hear about it. - Some accepted, some rejected → a
warningon the logger, and the send succeeds. Raising here would tell the caller the whole thing failed, and a retry would deliver twice to everyone who already had it.
Everything that leaves the transport implements Symfony's TransportExceptionInterface,
including API errors and connection failures, so the one catch a Symfony consumer
already writes is enough.
The SparkPost transmission id is recorded as the message id. Mailer::send() returns
void, so the SentMessage carrying it comes from the transport:
$sent = $transport->send($email); $sent->getMessageId(); // the transmission id, for matching against message events $sent->getDebug(); // "SparkPost transmission 1166…: 2 accepted, 0 rejected"
The transport dispatches the same events the mailer does. It returns null only when a
listener rejected the message before it was sent.
SparkPost-specific fields
Campaigns, metadata and substitution data are transmission-level concepts with no MIME
equivalent, so there is nowhere in a plain Email to put them. Use SparkPostEmail:
use Hampel\SparkPost\Transport\Mime\SparkPostEmail; $email = (new SparkPostEmail()) ->setCampaignId('welcome') ->setTransactional() ->setOpenTracking(false) ->setMetadata(['user_id' => 7]) ->setSubstitutionData(['first_name' => 'Alice']); $email->from('webmaster@example.com')->to('alice@example.com')->subject('Welcome')->text('…');
It is a plain Email in every other respect, and it survives serialisation — so a message
queued through Symfony Messenger arrives at the worker with all of the above intact.
Stored templates and A/B tests replace the message body entirely. The message still needs a From and a To, because the envelope is built from them:
(new SparkPostEmail())->setTemplate('welcome')->from(…)->to(…); (new SparkPostEmail())->setAbTest('subject-line')->from(…)->to(…);
Cc, Bcc, and what the recipient sees
SparkPost sends one message per recipient, so a naive transport gives every recipient a
To: line containing only themselves, and loses Cc entirely. This one does what ordinary
mail does: every recipient gets the same To: line, Cc is made visible with a CC header,
and Bcc gets no header at all — which is what makes it blind.
Testing against the sink
SparkPost accepts, counts and discards anything at <address>.sink.sparkpostmail.com, so a
staging site can exercise real sending, including bounce and delivery events, without mail
reaching anyone:
use Hampel\SparkPost\Transport\EventListener\SinkEnvelopeListener; use Symfony\Component\EventDispatcher\EventDispatcher; $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new SinkEnvelopeListener()); $transport = new SparkPostTransport($sparkpost, $dispatcher);
The dispatcher has to reach the transport, which is what invokes the listener. A listener added to a dispatcher the transport never received does nothing, and the mail is delivered normally.
It rewrites the envelope and leaves the headers alone, so the delivered message still reads as though it were addressed normally.
Symfony versions
^5.4|^6.4|^7.0, and the suite runs against all three. 5.4 is included deliberately: this
package is used inside host applications that bundle Symfony themselves at 5.4 and load
their own copy first, so a consumer there cannot substitute a newer one.
Licence
MIT. See LICENSE.md.