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 Mailer Laravel Package

symfony/postmark-mailer

Postmark integration for Symfony Mailer, supporting both SMTP and API transports via simple DSN configuration using your Postmark Server Token. Configure with postmark+smtp://ID@default or postmark+api://KEY@default.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the Package

    composer require symfony/postmark-mailer
    

    Laravel's MailManager discovers the postmark / postmark+smtp transports automatically — no custom transport class is required.

  2. Configure .env

    # API transport (recommended: templates, tags, metadata, bounce webhooks)
    MAIL_MAILER=postmark
    MAILER_DSN=postmark+api://YOUR_POSTMARK_SERVER_TOKEN@default
    
    # SMTP transport (alternative: plain SMTP relay)
    # MAILER_DSN=postmark+smtp://YOUR_POSTMARK_SERVER_TOKEN@default
    

    Keep the server token out of config/ — read it via env() only.

  3. First Use Case: Send a Test Email

    use Illuminate\Support\Facades\Mail;
    use App\Mail\TestEmail;
    
    Mail::to('[email protected]')->send(new TestEmail());
    

    Or drop down to Symfony's mailer for full header control:

    use Symfony\Component\Mailer\MailerInterface;
    use Symfony\Component\Mime\Email;
    
    $email = (new Email())
        ->from('[email protected]')
        ->to('[email protected]')
        ->subject('Test Email')
        ->text('Hello, this is a test!');
    
    app(MailerInterface::class)->send($email);
    
  4. Verify in Postmark Dashboard Check the Postmark web interface for the activity feed, delivery events, and raw message source.

Upgrade note (v8.1.6): if you consume Postmark webhooks (bounces, spam complaints, inbound parsing), pin ^8.1.6. The fix in #65674 makes the bridge authenticate webhook requests with the configured secret instead of ignoring it. On v8.1.0–v8.1.5 your configured credential was not applied, so signature/auth checks could pass or fail for the wrong reason.


Implementation Patterns

Core Workflows

1. Transactional Emails (High Priority)

Use the API transport for per-message error codes (ErrorCode / MessageID) that you can log and retry on; use SMTP only when you need a legacy relay path or very large attachments.

// config/mail.php
'postmark' => [
    'transport' => 'postmark',
],
Mail::to($user->email)->send(new PasswordResetMail($token));

2. Queued / Async Sends

Queuing is a Mailable concern in Laravel, not a mailer-config concern:

Mail::to($user->email)->queue(new WelcomeEmail($user));
// or: ->later(now()->addMinutes(5), new WelcomeEmail($user))

class WelcomeEmail extends Mailable implements ShouldQueue
{
    public $tries = 3;
    public $backoff = [10, 60, 300];
}

Serialize models, not closures, and keep attachments on disk (attachFromStorage) so queued payloads stay small.

3. Postmark Templates & Metadata

Symfony's bridge does not expose Postmark's server-side template IDs. Two supported options:

  • Render in Laravel (recommended): Blade/Markdown mailable → ->html(...), and keep Postmark for delivery.
  • Call the Postmark API directly via Http::withHeaders(['X-Postmark-Server-Token' => ...]) when you need TemplateId/TemplateModel.

Tag messages so you can filter the Postmark activity feed:

$email->getHeaders()->addTextHeader('X-Postmark-Tag', 'password-reset');

(Or set X-Postmark-Tag via a Mailable withSymfonyMessage() callback.)

4. Inbound Email / Webhook Handling

Route::post('/postmark/webhook', PostmarkWebhookController::class)
    ->middleware(['throttle:120,1']);
public function __invoke(Request $request)
{
    $payload = $request->json()->all();

    // v8.1.6+: the bridge now uses the secret you configured.
    // If you verify yourself, use Postmark's Basic Auth credentials:
    // abort_unless(hash_equals($expected, $request->getPassword()), 403);

    match ($payload['RecordType'] ?? null) {
        'Bounce'          => $this->handleBounce($payload),
        'SpamComplaint'   => $this->handleComplaint($payload),
        'Inbound'         => $this->handleInbound($payload),
        'Delivery'        => null, // no-op
        default           => null,
    };

    return response()->noContent();
}

Configure the same username/password in the Postmark dashboard and in your DSN/webhook config so both sides agree.

5. Error Handling & Retries

use Symfony\Component\Mailer\Exception\TransportExceptionInterface;

try {
    Mail::to($user->email)->send(new ReceiptMail($order));
} catch (TransportExceptionInterface $e) {
    report($e);
    // Postmark rejections are usually permanent (422) — don't blindly retry.
}

Integration Tips

  1. No custom transport needed. Laravel registers the Symfony bridge via symfony/mailer's transport factory; adding a PostmarkTransport singleton in AppServiceProvider will shadow Laravel's and break Mail::fake()/Mail::assertSent().
  2. Testing — Mail::fake() for application tests, and symfony/mailer's TestMailer when you want to assert on the raw Symfony Email object (headers, CIDs, parts).
  3. Multiple senders — define a second mailer in config/mail.php (e.g. postmark_marketing) with a different server token; select it with Mail::mailer('postmark_marketing').
  4. Webhook idempotency — Postmark retries; dedupe on MessageID + RecordType in a unique index rather than assuming single delivery.

Gotchas and Tips

Pitfalls

  1. Webhook secret was ignored before v8.1.6 The bridge now authenticates incoming webhook requests with the configured secret (#65674). If your webhook endpoint stopped or started rejecting traffic after an upgrade, re-check the credentials configured on both the Postmark side and your mailer/webhook config — don't "fix" it by disabling verification.

  2. Date header dropped in API transport The API transport strips Date before sending. Add it manually only if a downstream consumer requires it:

    $email->getHeaders()->addDateHeader('Date', new \DateTimeImmutable());
    
  3. Inline attachments / CID references Use ->embed() / ->embedFromPath() so Symfony generates matching Content-ID headers; hand-rolled CID headers are the usual cause of broken images. Inspect the raw source in the Postmark dashboard to confirm.

  4. Payload size Postmark caps message size (~10 MB). Keep attachments in object storage and send links, or fall back to SMTP for the rare large send.

  5. SMTP vs API throughput Postmark rate-limits per server token. API transport gives you explicit 429/ErrorCode responses you can retry with backoff; SMTP just blocks or drops. For high volume, pair queued mailables with a rate-limited queue.

  6. MAIL_MAILER vs MAILER_DSN Laravel builds the transport from MAIL_MAILER; the DSN belongs to symfony/mailer. Mixing the two (setting only MAILER_DSN) is a frequent "it's still sending via log" cause.

Debugging

  1. Verbose transport logging

    MAIL_MAILER=postmark
    MAIL_LOG_CHANNEL=stack
    

    Wire a logger into the mailer and tail storage/logs/laravel.log for TransportException output.

  2. Raw API probe

    curl -X POST https://api.postmarkapp.com/email \
         -H "X-Postmark-Server-Token: YOUR_TOKEN" \
         -H "Content-Type: application/json" \
         -H "Accept: application/json" \
         -d '{"From":"[email protected]","To":"[email protected]","Subject":"Test","HtmlBody":"<p>Hello</p>"}'
    

    Isolates token/account problems from Laravel config problems.

  3. Common errors

    Error Cause Fix
    Invalid Server Token (401) Wrong token in DSN / wrong server Verify token belongs to the intended Postmark server
    422 Inactive recipient Address suppressed by a prior bounce/complaint Reactivate in Postmark, or handle in app logic
    Payload Too Large Message > ~10 MB Link attachments or use SMTP
    Date header missing API transport strips it Add Date header explicitly
    Webhook returns 401/403 Secret not configured or mismatched (fixed in 8.1.6) Align Postmark webhook credentials with your config
    Mail::assertSent() fails Custom transport binding shadows Laravel's Remove the manual TransportInterface singleton
  4. Extension point Subclass the bridge's PostmarkApiTransport only if you must inject custom headers or retry logic; register it via a custom mailer in config/mail.php ('transport' => 'postmark' subclasses can be bound through Mail::extend()), not by rebinding TransportInterface globally.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings