Skip to content

Releases: stechstudio/laravel-postmaster

v2.25.2

Choose a tag to compare

@jszobody jszobody released this 13 Sep 21:46

Preserve declared model relationships and content preferences when Symfony transports clone outbound messages. Private tracking headers remain stripped. Metadata expires with failed sends instead of being reused for later messages.

v2.25.1

Choose a tag to compare

@jszobody jszobody released this 13 Sep 21:38

Preserve delivery history when callbacks arrive before the send response. Serialize message updates, retain precise event timestamps, and prevent stale callbacks from regressing delivery status. Run migrations after updating; use a shared cache store across application instances.

v2.25.0 — Attachments live in one place

Choose a tag to compare

@jszobody jszobody released this 10 Aug 15:51

A correction to v2.24.0, released the same day.

Attachments live in one place

v2.24.0 moved attachments to their own email_attachments table, and renamed
the old filenames column on email_messages to legacy_attachment_names so
the new relation could take the name.

That was the wrong call. The column is still created by the migration that
builds the table, so a fresh install made the column and renamed it in the same
migrate run — leaving every new install with a column named "legacy" on day
one that could never hold anything.

It's dropped now. Attachments live in email_attachments and only there.

Upgrading

Run php artisan migrate. The drop is guarded and handles every state:

Coming from What happens
A fresh install The column is never created. Nothing to drop.
v2.24.0 legacy_attachment_names is dropped.
v2.23.0 or earlier attachments is renamed, then dropped.

Breaking: EmailMessage::legacyAttachmentNames() is removed, along with
the column behind it. It existed for one release.

The only rows that could have held anything are ones recorded before v2.24.0
with content storage on, and they held filenames only — never the files
themselves. Nothing that was ever stored on disk is affected. If you want to
keep those names, copy them out before migrating:

select id, legacy_attachment_names from email_messages
where legacy_attachment_names is not null
  and legacy_attachment_names != '[]';

Everything else in v2.24.0 — storing attachment bytes, downloads, signed URLs
on cloud disks, the dashboard changes — is unchanged.

v2.24.0 — Attachment storage

Choose a tag to compare

@jszobody jszobody released this 10 Aug 15:22

Postmaster can now keep the files an email carried, not just the fact that it
carried them.

Storing attachments

Attachment metadata — filename, type, size — is recorded whenever either
storage switch is on. The bytes are a separate opt-in, independent of
POSTMASTER_STORE_CONTENT, which is the point: an invoice PDF is often worth
keeping when the body that carried a magic-login link is not.

POSTMASTER_STORE_ATTACHMENTS=true
POSTMASTER_ATTACHMENTS_DISK=s3

With it on, Resend and Release replay a message with its attachments intact,
and the dashboard offers each one as a download.

Bytes are content-addressed by sha256, so the logo on every send costs one
file however many messages carry it, and a file is only removed once every
message referencing it has let it go.

Three limits keep the disk bounded — a per-file ceiling, a retention window,
and a total budget that evicts oldest-first. The metadata row always survives,
so the dashboard still reports what an email carried even after the bytes are
gone.

Downloads, and S3

Downloads always go through Postmaster's own gated endpoint, which authorizes
the request before handing anything out. From there a cloud disk gets a
redirect to a short-lived signed URL, so the bytes travel from the bucket
rather than through a PHP worker; a local disk streams through the app, where
a redirect would save nothing.

Postmaster sets no ACL on what it writes, so a bucket using S3's Bucket owner
enforced
ownership setting — the default since 2023 — works untouched. The
readme documents the storage prefix, for scoping a lifecycle rule or a bucket
policy.

In the dashboard

  • A paperclip in the message list marks mail that went out with something
    attached.
  • The message detail page lists the files between the envelope details and the
    body, where a mail client puts them.
  • Embedded images aren't listed — they're resolved back into the preview
    instead, and the preview says so when one is no longer stored.
  • The message page now reads as one message rather than a stack of cards, the
    body sizes itself to its content, and the overview chart names each bar's
    count on hover.

Upgrading

Run php artisan migrate. Two migrations land: the new email_attachments
table, and a rename of the attachments column on email_messages to
legacy_attachment_names. The rename is data-preserving — that column held
filenames only, and the dashboard still reads them — and it frees the name for
the new relation.

Nothing else changes. With POSTMASTER_STORE_ATTACHMENTS left off, the only
new behaviour is that attachment metadata is recorded alongside stored content.

v2.23.0

Choose a tag to compare

@jszobody jszobody released this 07 Jul 19:08
ab6ab16

Non-interactive install now reports the full webhook-auth setup

The report used to tell you the webhook URL but not that you also have to configure the matching auth — and a registered URL alone gets rejected, since every provider authenticates inbound webhooks and the authorizers fail closed on a missing credential.

It now always prints that provider's webhook-auth setup:

  • which credential it uses and whether it's set in your .env,
  • what to configure on the provider side so its webhooks authenticate (Postmark basic-auth / token on the webhook; SendGrid / Mailgun / Resend signing key from the dashboard; SES SNS subscription),
  • a warning that inbound webhooks are rejected until the credential is set, when it's missing.

Secret values are never echoed — only env var names — so it's safe to run in a deploy log. The guidance is rendered in full (no longer truncated in an ~80-column log).

Internal: ProviderSetup::authFailureGuidance() is renamed to webhookAuthGuidance(), now serving both install setup and verify's failure diagnosis. See #30.

v2.22.3

Choose a tag to compare

@jszobody jszobody released this 07 Jul 18:42
d35b273

Fix: Resend / Release no longer 500 on a failed send

Both dashboard actions called the mailer with no error handling, so if the send failed — most commonly sandbox mode on locally with no real mail provider configured — the exception propagated to an unhandled 500.

They now catch it: the error is reported to your logs, the per-message throttle is cleared so a retry isn't blocked, and you get a flash error (Release failed — the email could not be sent. <reason>) instead of a 500. A failed release leaves the message untouched — still sandboxed, releasable again once mail works.

With the typical local log/array mailer there was never an error (the email just goes to the log); this only affected real provider transports without valid credentials. See #29.

v2.22.2

Choose a tag to compare

@jszobody jszobody released this 07 Jul 18:19

Two small improvements to the setup commands and the dashboard.

postmaster:verify defaults the recipient to MAIL_FROM_ADDRESS

Omitting --to used to error out. It now falls back to your app's own from-address — a real, owned inbox — so the check can run with no arguments (handy in a deploy hook). Emailing yourself is a natural test, and even a bounce there still proves the webhook path. --to still overrides; it only errors when neither is set. (#28)

php artisan postmaster:verify            # sends to MAIL_FROM_ADDRESS
php artisan postmaster:verify --to=you@example.com

Cache-busted dashboard assets

The stylesheet, Alpine bundle, and logo are served from stable routes with heuristic caching, so after upgrading the package a browser could keep serving the old files until a hard refresh (stale styles, button layout, etc.). Each asset URL now carries a short content hash — when a file changes on upgrade, its URL changes and the browser fetches the new copy automatically.

v2.22.1

Choose a tag to compare

@jszobody jszobody released this 07 Jul 17:39
fc88479

Fix: non-interactive install / verify crashed on no-TTY platforms

v2.22.0 gated interactivity on $input->isInteractive(), which reports true on platforms without a TTY (Laravel Cloud). Laravel Prompts additionally requires a real TTY, so postmaster:verify (and install) took the interactive branch and then hit In Interactivity.php line 32: Required. with a failed exit code.

Both commands now detect interactivity the way Laravel configures Prompts — an interactive input and a real TTY — so a no-TTY run correctly routes to the non-interactive path. Omitting --to now gives a clean Pass --to=you@example.com … error instead of a crash.

php artisan postmaster:verify --to=you@example.com --provider=postmark

Upgrade from v2.22.0 if you run these commands in Laravel Cloud or CI. See #27.

v2.22.0

Choose a tag to compare

@jszobody jszobody released this 07 Jul 17:13
4a82a63

Non-interactive install and verify

Both setup commands now work where there's no terminal — Laravel Cloud, CI, deploy hooks — which previously wasn't possible (the interactive prompts hang or error without a TTY). Detected automatically from a missing TTY, or forced with -n / --no-interaction. A new --provider= option overrides auto-detection.

postmaster:verify

Runs the full delivery-webhook round trip with no prompts:

php artisan postmaster:verify --no-interaction --to=you@example.com --provider=postmark

--to= sets the test recipient; the "have you set the webhook?" confirmation is skipped; the live watch prints without the spinner (clean in logs); and the exit code reflects the result (0 on a delivery, non-zero on a timeout/failure/auth-rejection) — so it works as a deploy or CI gate.

postmaster:install

Can't collect credentials without prompting, so it becomes a setup report — reads the configured provider from your environment and prints the webhook URL, how to point the provider at it, whether the webhook-auth credential is set, and the current feature flags. Writes nothing.

php artisan postmaster:install --no-interaction --provider=sendgrid

Fails only when no provider can be determined; a missing webhook credential is surfaced as a warning.

Full suite: 320 tests, 908 assertions. See #26. Also includes the README polish from #25.

v2.21.0

Choose a tag to compare

@jszobody jszobody released this 07 Jul 16:12
72f1e35

Builds on the sandbox Release action from v2.20.0 with new dashboard capabilities and important fixes.

New

  • Delete a message (#24). A Delete button on the message detail page removes a record from the stored history — for scrubbing PII or purging something that shouldn't have been kept. It deletes the message and its timeline; resends of it survive (their link is nulled), and other recipients of the same email are separate records, left untouched. The confirm dialog is explicit that this only removes Postmaster's record — it does not recall or unsend an email that already went out.
  • postmaster:verify runs under sandbox mode (#21). Instead of refusing when POSTMASTER_DELIVERY=sandbox, verify now warns that delivery is sandboxed and sends a single test email that bypasses the sandbox — so you can confirm the whole webhook round trip before switching delivery to normal. Your delivery setting is left unchanged.

Fixed

  • Release no longer creates a duplicate message under a real provider transport (#23). The marker that identified a release could fail to survive the send with a real transport (it held with the log/array mailers, which hid the flaw), causing a brand-new message to be written instead of the sandboxed row being reconciled in place. Release now flags itself out-of-band for the duration of its synchronous send, so it can't be lost in transit — regardless of provider.
  • Resend is gated on the message, not the delivery mode (#22). A message that was released — and so is genuinely sent — is now resendable like any other sent message, even while sandbox mode is on. A sandboxed message still shows Release instead of Resend.

Full suite: 309 tests, 884 assertions. See #21, #22, #23, #24.