Skip to content

Add Resend inbound email driver - #144

Open
jasonfish568 wants to merge 14 commits into
beyondcode:masterfrom
jasonfish568:codex/resend-inbound-driver
Open

Add Resend inbound email driver#144
jasonfish568 wants to merge 14 commits into
beyondcode:masterfrom
jasonfish568:codex/resend-inbound-driver

Conversation

@jasonfish568

@jasonfish568 jasonfish568 commented Jul 29, 2026

Copy link
Copy Markdown

Summary

This adds a Resend inbound driver to Laravel Mailbox.

Resend differs from the existing inbound providers: its email.received webhook
contains metadata and an email_id, but not the raw email. The driver must
therefore verify the webhook, retrieve the receiving record from Resend, and
download the raw MIME message before it can reuse Laravel Mailbox's existing
InboundEmail::fromMessage() pipeline.

The processing flow is:

  1. verify the Svix signature against the original, unmodified request body
  2. validate the event and extract the signed data.email_id
  3. dispatch a ProcessResendEmail job on the configured queue connection
  4. retrieve the email through Resend's Receiving API
  5. download the raw MIME message without forwarding the API bearer token to the
    download URL
  6. parse the message and call the existing mailbox handlers

Why queue processing is included

Processing a Resend webhook requires two outbound HTTP operations—the Receiving
API lookup and the raw MIME download—followed by MIME parsing. Messages may also
contain large attachments. Doing all of that inside the webhook request can
make response time depend on Resend, the download host, and message size.

The queue integration provides:

  • a sync default, so existing and low-volume applications need no worker
  • an asynchronous option for production/high-volume applications, allowing the
    webhook to return HTTP 200 after the job has been accepted
  • Laravel retry/backoff behavior for temporary API, download, or mailbox
    processing failures
  • a 180-second job timeout suitable for downloading and parsing complete MIME
    messages

Webhook and queue delivery remain at-least-once. The driver intentionally does
not add a long-lived unique-job lock; mailbox handlers with side effects should
use a stable business key such as the raw email's Message-Id for idempotency.

Why the HTTP, Guzzle, Bus, Queue, and Cache dependencies are declared

These are runtime dependencies used directly by the driver, rather than
unrelated framework additions:

Dependency Purpose
illuminate/http Laravel's HTTP client API for authentication, timeouts, error handling, response parsing, and test fakes
guzzlehttp/guzzle The transport used by Laravel's HTTP client to perform the Receiving API lookup and raw MIME download
illuminate/bus Laravel's job dispatch and Queueable support
illuminate/queue ShouldQueue, queue connection selection, retries, backoff, middleware, and worker integration
illuminate/cache Laravel's shared RateLimiter, used to keep concurrent workers within Resend's per-team API allowance

Full Laravel applications normally already contain these framework components
and Guzzle. Declaring them explicitly prevents the package from relying on
undeclared transitive dependencies when it is installed with individual
Illuminate components.

The driver uses Laravel's existing HTTP client instead of adding the Resend PHP
SDK. Webhook verification is implemented with the documented Svix HMAC format,
PHP's built-in hashing functions, constant-time comparison, and timestamp
validation. This keeps the dependency surface focused on components already
used by Laravel applications.

Rate limiting and high-volume operation

Resend's default API allowance is five requests per second per team. Multiple
queue workers could otherwise exceed that limit even when each worker is
healthy. MAILBOX_RESEND_RATE_LIMIT therefore defaults to 5, and the job uses
Laravel's cache-backed rate limiter before calling the Receiving API.

Applications running workers on multiple nodes should use a shared cache store
with atomic increments, such as Redis. The limiter is only for the external API
rate; it is not used as a webhook deduplication lock.

What changed

  • register a resend mailbox driver and webhook route using the repository's
    existing invokable-controller style
  • verify svix-id, svix-timestamp, and svix-signature against the raw body
  • validate signed webhook payloads before dispatching work
  • retrieve and download raw MIME messages with explicit timeouts and without
    forwarding the API key to the download URL
  • support synchronous processing by default and configurable asynchronous queue
    processing for higher volume
  • apply retry/backoff behavior and a configurable shared per-second API rate
    limit
  • merge Resend defaults into older published mailbox configuration files
  • document setup, queue configuration, rate limiting, and handler idempotency
  • add controller, client, job, middleware, signature, driver, configuration,
    MIME, attachment, retry, duplicate-delivery, and rate-limit coverage

Validation

  • composer test
    • 102 tests
    • 218 assertions
  • the complete package suite passes with Laravel 10, 11, 12, and 13 dependency
    sets (102 tests and 218 assertions in each environment)
  • real Laravel 10, 11, 12, and 13 applications were verified through a public
    Cloudflare tunnel and Resend Receiving
  • each live run covered webhook receipt, database queue dispatch, Receiving API
    retrieval, raw MIME parsing, text and HTML bodies, attachment integrity, and
    inbound email persistence

@jasonfish568
jasonfish568 marked this pull request as ready for review July 29, 2026 07:24
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.

1 participant