Skip to content

feat!: Lettermint Laravel 3.0 - #45

Merged
bjarn merged 9 commits into
mainfrom
feat/v3
Oct 4, 2026
Merged

bjarn merged 9 commits into
mainfrom
feat/v3

Conversation

@bjarn

@bjarn bjarn commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Laravel package 3.0, built on the new PHP SDK 3.0, following the same design as the other SDK majors.

What changes

  • Built on lettermint/lettermint-php ^3.0 (feat!: Lettermint PHP SDK 3.0 lettermint-php#43). The container holds one shared Lettermint\Lettermint client; the facade exposes Lettermint::emails(), ::domains() and so on.
  • Mail transport: a fresh payload per message, so nothing leaks between mails in a queue worker. In 2.x a failed mail left its attachments, tag, metadata and idempotency key on a shared builder.
  • Webhooks:
    • Verified with the SDK. The delivery header is required, and a failed check returns 401 with a reason.
    • Unknown event types return 200 and dispatch UnknownWebhookEventReceived.
    • The new Contracts\WebhookEvent interface lets one listener receive every event.
    • Webhook data is tolerant: fields the API may omit are nullable and $event->payload holds the raw payload. This fixes the 2.x crash on inbound attachments sent as URLs.
  • Config keys are unchanged. LETTERMINT_PROJECT_TOKEN / LETTERMINT_TEAM_TOKEN are read, with the old env names as fallbacks. A timeout of 0 is now rejected.
  • PHP ^8.2 and Laravel 10–13.
  • UPGRADE.md has before/after for every change, the nullable-fields table, and a ready-to-copy coding-agent instruction. 2.x no longer receives updates.

Verification

  • 288 Pest tests pass. PHPStan and Pint pass.
  • All 26 CI matrix cells pass locally against the local PHP SDK 3.0.

Release

Merge and release after lettermint/lettermint-php 3.0.0 (lettermint/lettermint-php#43) is on Packagist. Until then CI fails at dependency install. Afterwards composer update resolves ^3.0 with no file changes. Then tag 3.0.0.

Docs for all SDK majors: lettermint/lettermint#2594 (stacked on lettermint/lettermint#2590). Merge it after the releases.

🤖 Generated with Claude Code

bjarn added 9 commits October 3, 2026 13:51
Replace the builder mocks with a real EmailEndpoint backed by an HTTP-level
recording client, so the tests assert the exact JSON body and request
headers that reach the Lettermint API.
The transport lives for the whole mailer lifetime (e.g. a queue worker) and
drove the shared EmailEndpoint builder field by field. The SDK only resets
that builder inside send(), so an error between the first builder call and
send() left the tag, metadata, attachments and Idempotency-Key on the
builder, and the next mail sent by that worker inherited them.

doSend() now builds the complete payload as a local array and sends it
through EmailEndpoint::send($payload) (available since lettermint-php
2.0.0). The idempotency key is set immediately before send(), which always
clears it. The JSON body and request headers are unchanged for every
existing case (covered by the characterisation tests).

Attachments without a filename and emails without a subject are rejected
with a TransportException before any request is made, instead of a
TypeError (the API requires both fields).
The controller resolved the event type with WebhookEventType::from(), so
any event type added to the API after the installed package version threw
a ValueError and returned HTTP 500. Lettermint then retries the delivery
and may disable the endpoint.

Unknown, missing or non-string event names now return 200 and dispatch a
new UnknownWebhookEventReceived event carrying the raw event name and the
full verified payload. Known event types dispatch their typed events
exactly as before.
…ery webhook event

Laravel matches listeners on an event's own class and its interfaces, not
parent classes, so listening on the abstract LettermintWebhookEvent never
fired. Typed events and UnknownWebhookEventReceived now implement
Contracts\WebhookEvent, and the README documents the working approach.
Require lettermint/lettermint-php ^3.0 and replace the 2.x SDK entry points.

- The container binds one Lettermint\Lettermint client (singleton, alias
  "lettermint") built from the project token, the team token or both.
  Empty config values count as missing; with no token at all resolving the
  client throws ApiTokenNotFoundException::noTokens(). An optional Guzzle
  client can be bound under LettermintServiceProvider::HTTP_CLIENT.
- The Lettermint facade resolves to that client and exposes its parts as
  methods: Lettermint::emails()->send(...), Lettermint::domains()->list().
- The mail transport sends each locally built payload with
  $client->emails->send($payload, idempotencyKey: $key) and maps every
  LettermintException to a TransportException whose code is the HTTP
  status (0 when no response was received). The request body is unchanged.
- The team token is read from LETTERMINT_TEAM_TOKEN, falling back to
  LETTERMINT_API_TOKEN; the config keys are unchanged. The timeout is a
  float.
- VerifyWebhookSignature verifies with the SDK 3.0 Webhook: the request
  HeaderBag is passed as is, X-Lettermint-Delivery is required, the 401
  body carries the failure reason, and the request attribute holds a
  WebhookPayload instead of an array.

BREAKING CHANGE: Requires lettermint/lettermint-php 3.0. The EmailEndpoint
and ApiClient bindings, the lettermint.api alias, the empty
Lettermint\Laravel\Lettermint class and TeamApiTokenNotFoundException are
removed; the facade and the "lettermint" alias resolve to
Lettermint\Lettermint. The transport constructor takes the client. Webhook
deliveries without X-Lettermint-Delivery or a string event name are
rejected with 401, and the verified payload attribute is a WebhookPayload.
Webhook data objects no longer throw when the API omits a field, adds one
or sends a value of another type, so schema drift cannot turn a delivery
into a 500 that Lettermint retries.

- Every DTO reads its fields through an internal Field reader: a missing
  or mistyped value reads as null (or an empty list, or the documented
  default) instead of raising an error.
- Fields the API may omit or send as null are nullable, for example
  subject, reason, score, spamScore, statusCode, linkIndex, the event dates
  and the scheduling timestamps. Identifiers the API always sends
  (messageId, recipient, route, suppression fields, envelope id) stay
  strings and read as '' if absent.
- Inbound attachments delivered as signed URLs decode: EmailAttachment has
  url and expiresAt, and content is null for them.
- Typed events carry the complete verified payload as $event->payload, so
  fields the DTOs do not map (context, sandbox, tags, ...) stay reachable.
- A test pins WebhookEventType to the PHP SDK's WebhookEvent values.

BREAKING CHANGE: Several webhook DTO properties are now nullable (see
UPGRADE.md), WebhookEnvelope::$timestamp is nullable, and
EmailAttachment::getDecodedContent() returns null for URL attachments.
Test the declared minimum, PHP 8.2, and the current PHP 8.5, besides 8.3
and 8.4. Skip the combinations Laravel does not support: Laravel 13 needs
PHP 8.3 or later, and Laravel 10 and 11 do not support PHP 8.5.
Document the shared Lettermint client and facade, LETTERMINT_PROJECT_TOKEN
and LETTERMINT_TEAM_TOKEN, the typed transport errors, queued-job and
custom HTTP client usage, the required webhook headers and failure
reasons, the tolerant webhook data and the raw event payload.

UPGRADE.md covers every changed call with before/after examples, the
nullable webhook data properties, the removed bindings and classes, and an
instruction for upgrading with a coding agent. 2.x no longer receives
updates, including fixes.
@bjarn
bjarn requested a review from a team as a code owner October 3, 2026 22:51
@bjarn
bjarn enabled auto-merge (squash) October 4, 2026 11:22
@bjarn
bjarn merged commit 11e3e8f into main Oct 4, 2026
30 of 57 checks passed
@bjarn
bjarn deleted the feat/v3 branch October 4, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants