Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Postmark Swiftmailer Bundle Laravel Package

uam/postmark-swiftmailer-bundle

A Symfony bundle that plugs Postmark into Swiftmailer, adding a mail transport so you can send transactional email through Postmark's API using standard Symfony service configuration. Keeps Swiftmailer's spooling, events, and existing mailer workflow intact.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony 2.x bundle that plugs Postmark's HTTP API into SwiftMailer as a custom Swift_Transport. Conceptually you are not "using a package" day-to-day: you configure it once, then keep writing ordinary Swift_Message code and let the transport swap SMTP for POST https://api.postmarkapp.com/email. Treat it as infrastructure, not an API surface.

Prerequisites

  • Symfony 2.x app with SwiftmailerBundle enabled (this bundle predates Symfony 3/4, and SwiftMailer itself is EOL since Nov 2021 — see Gotchas).
  • A Postmark account with a verified sender signature or domain. Nothing works until this is done; the transport will happily POST and Postmark will happily return 422.
  • A Postmark Server API Token (Servers → your server → API Tokens), not the account token.

Install

Because the package has 2 stars and no dependents, assume it may not resolve cleanly from Packagist. Add it as a VCS repository first:

"repositories": [{ "type": "vcs", "url": "https://github.com/opichon/UAMPostmarkSwiftMailerBundle" }],
"require": { "uam/postmark-swiftmailer-bundle": "dev-master" }

Register and configure

// app/AppKernel.php
new UAM\PostmarkSwiftMailerBundle\UAMPostmarkSwiftMailerBundle(),
# app/config/parameters.yml
parameters:
    postmark_api_key: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# app/config/config.yml
uam_postmark_swift_mailer:
    api_key: "%postmark_api_key%"

swiftmailer:
    transport: postmark   # use whatever value the bundle README names

First thing to do: read the bundle's DI extension and Resources/config/services.xml. The exact config key, transport service id, and whether it uses the transport: string or a compiler pass to replace swiftmailer.transport are the two facts you must confirm before anything else. If swiftmailer: transport: postmark is rejected by your SwiftmailerBundle version (older versions only switched on smtp|sendmail|mail|null), alias the service yourself — app config loads after bundle config, so your definition wins:

# app/config/config.yml
services:
    swiftmailer.transport:
        alias: uam_postmark.transport   # confirm real id in services.xml

First use case: a password-reset mail from an existing controller — it is the cheapest end-to-end proof because it exercises From/To, HTML + text bodies, and a real recipient:

$message = \Swift_Message::newInstance()
    ->setSubject('Reset your password')
    ->setFrom('[email protected]')      // must be a verified signature
    ->setTo($user->getEmail())
    ->setBody($html, 'text/html')
    ->addPart($text, 'text/plain');

$this->get('mailer')->send($message);

Then open Postmark → Activity and confirm the message appears with a Delivered status. Activity is the source of truth; the transport's return value is just a recipient count.

Implementation Patterns

Keep sending through the mailer service. The transport is transparent to application code. Do not inject the Postmark transport into controllers — inject mailer (or better, a small AppMailer service) so the transport stays a deployment concern.

Environment matrix. This is where the bundle earns its keep:

# config_dev.yml
swiftmailer:
    disable_delivery: true    # nothing leaves the machine

# config_test.yml
swiftmailer:
    transport: null           # or disable_delivery, then assert on the profiler

Only config_prod.yml inherits the Postmark transport. Because the bundle still builds the transport object in dev, keep the API key in parameters.yml (git-ignored) so the container compiles without a real secret.

Spool anything user-facing or batched. The transport is synchronous HTTP: one request per message, on the request thread. For registration emails that is fine; for a newsletter or an import it is not.

swiftmailer:
    spool: { type: file, path: "%kernel.root_dir%/spool" }
*/5 * * * * php app/console swiftmailer:spool:send --env=prod --message-limit=200

The cron worker is also your natural rate limiter: Postmark throttles per-server (~ messages/minute), and the transport almost certainly has no backoff. Tune --message-limit against your server's limit rather than adding retry logic.

Wrap composition, not transport. Put message building in a service that takes Twig_Environment + mailer, render templates there, and let Symfony's SwiftMailer integration (Swift_Message, addPart) do MIME work. This keeps you portable if you later migrate to symfony/postmark-mailer.

Log the send boundary. Register a tiny listener on Swift_Events_SendEvent (via swiftmailer.plugin or a kernel listener) that records subject, recipient count, and elapsed time. You will not get a Postmark MessageID back through a stock Swift transport — correlate via Activity or via your own X-PM-* headers if the transport forwards custom headers (verify this; many naive transports drop them).

Extension point for Postmark-specific fields. Tag, TrackOpens, TrackLinks, and (newer) MessageStream have no SwiftMailer analogue. The cleanest day-to-day approach is a thin subclass that overrides send() and adds them to the payload, registered in place of the bundle's service:

class TaggedPostmarkTransport extends \UAM\PostmarkSwiftMailerBundle\Transport\PostmarkTransport
{
    protected function buildPayload(\Swift_Mime_Message $message)
    {
        $payload = parent::buildPayload($message);   // confirm method name in source
        $payload['Tag'] = 'transactional';
        $payload['TrackOpens'] = true;
        return $payload;
    }
}

If the transport has no separable payload method, decorate it instead: your class implements Swift_Transport, delegates to the bundle transport, and post-processes. Subclassing a 2-star bundle's protected API is safer than editing vendor code, but pin the commit you tested.

Bounces and complaints are not your app's job here. Postmark webhooks → an endpoint in your app is the supported path. The transport only tells you the API accepted the message (200), not that it was delivered.

Gotchas and Tips

The number-one failure is a 422 with ErrorCode 300: "Invalid 'From' address". Postmark requires From to match a verified Sender Signature or a verified domain. setFrom() with a random @gmail.com in a hurry is the classic. Check Postmark → Signatures before you debug a single line of PHP.

Vendor risk is real, and it is the headline. 2 stars, 0 dependents, Symfony 2 (EOL), SwiftMailer (EOL). Do not adopt for new work — use symfony/postmark-mailer on Symfony 4.4+/5+/6+ with Symfony Mailer. Adopt this only to retrofit Postmark into a legacy Symfony 2 app that must keep swiftmailer:spool:send semantics. Budget for forking the transport if it breaks on a PHP/SwiftMailer upgrade; check the class's type hints (Swift_Mime_Message vs Swift_Mime_SimpleMessage) against your installed SwiftMailer major before assuming compatibility.

The 10 MB limit is on the whole message, and base64 inflates attachments ~33%. A 7 MB PDF is the practical ceiling. Postmark also caps recipients per message (50 via the API) — setBcc() with 200 addresses will be a single rejected request, not 200 deliveries. Chunk in application code.

MIME extraction is the likely bug surface. Postmark's API wants discrete HtmlBody, TextBody, and base64 Attachments. A hand-rolled transport must decide what to do with multipart/mixed containing multipart/alternative plus attachments, and with inline images (Content-ID / cid:). Verify: (a) both HTML and text arrive, (b) attachments open, (c) inline images render. If inline images are broken, fall back to hosted images rather than fighting the transport.

Subject encoding. Swift builds RFC 2047 encoded subjects (=?UTF-8?Q?...?=). If the transport copies the raw header into Postmark's Subject field, recipients see mojibake. Look for getSubject() (decoded) in the source; if it uses getHeaders()->get('Subject')->getFieldBody(), you have a bug to patch.

Errors surface as Swift_TransportException; read the body. Postmark returns structured JSON (ErrorCode, Message) with a 4xx. If the exception message is just "Unprocessable Entity", wrap the HTTP call or enable logging of the response body — otherwise you are blind. Common codes: 300 invalid From, 406 inactive recipient, 422 general validation.

No retries, no idempotency. A 429/500 throws, and with the spool the worker stops on the exception — messages already sent are removed, the failing one is retried on the next cron run (duplicate risk only if the failure is post-acceptance). Keep --message-limit low enough that one bad message does not stall the queue for hours.

Config cache bites on Symfony 2. Changing parameters.yml or the transport service requires php app/console cache:clear --env=prod. A stale token in app/cache looks exactly like a bad token in Postmark.

Transport construction is eager. If your HTTP client is file_get_contents + stream context (common in bundles of this vintage), you need allow_url_fopen=1, a sane openssl.cafile/CA bundle, and a proxy configured if you are behind one. If it uses cURL, set CURLOPT_TIMEOUT — the default can hang a web request for minutes on a Postmark outage. Check php -i | grep openssl before blaming the package.

Return-Path, custom headers, and Message-ID are at the transport's discretion. Postmark sets its own MessageID; Return-Path is ignored in favour of the Return-Path configured on the sender domain. If your app parses Message-ID for threading, it will not match what Swift generated.

Testing pattern that actually works:

// functional test
$this->get('swiftmailer.mailer')->getTransport()->stop();
// assert via the profiler's mailer panel, or swap in an in-memory spool
$this->assertCount(1, $this->get('swiftmailer.mailer')->getTransport()->getSpool()->flushQueue(...));

Prefer disable_delivery: true in config_test.yml and assert on the collected messages rather than mocking the transport — the transport's internals are not worth coupling to.

Debug checklist, in order: Postmark Activity (did the request arrive?) → response body/ErrorCode (was it rejected?) → Sender Signature/domain verification (is From legal?) → config cache (cache:clear) → service alias (swiftmailer.transport actually pointing at Postmark, not SMTP). That order resolves ~90% of "it silently does nothing" reports with this bundle.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums