Search by

ojessecruz / resend-inbox

ojessecruz

Framework-agnostic core of a Resend-powered shared inbox: webhook parsing, conversation threading, reply headers and auto-reply detection.

Package info

github.com/ojessecruz/resend-inbox

pkg:composer/ojessecruz/resend-inbox

Statistics

Installs: 258

Dependents: 2

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-10-09 20:20 UTC

This package is auto-updated.

Last update: 2026-10-09 20:20:40 UTC


README

Framework-agnostic core of a shared inbox on top of Resend inbound email.

It decides; your framework integration stores, queues and renders:

  • Webhooks: verifies the Svix signature and turns the body into a typed event.
  • Threading: finds the conversation an inbound email belongs to, by In-Reply-To / References, falling back to the same subject with the same correspondent.
  • Replies: builds an email from Markdown with our own Message-ID and the threading headers, allowing only configured senders.
  • Auto-replies: recognizes auto-responders, bounces and bulk mail, so they never raise alerts.
  • Delivery status: orders sent → delivered → bounced/complained so late webhooks never move it back.
  • Resend API: fetches received emails and attachment URLs, and sends.

Depends only on resend/resend-php, symfony/mime (address parsing), egulias/email-validator and league/commonmark. No framework.

For Laravel, use ojessecruz/laravel-resend-inbox, which builds on this package.

Installation

composer require ojessecruz/resend-inbox

Usage

Webhooks

use Jessecruz\ResendInbox\Exceptions\InvalidWebhook;
use Jessecruz\ResendInbox\Webhook\DeliveryStatusChanged;
use Jessecruz\ResendInbox\Webhook\EmailReceived;
use Jessecruz\ResendInbox\Webhook\WebhookParser;

$parser = new WebhookParser($signingSecret); // "whsec_..." from the Resend dashboard

try {
    $event = $parser->parse($rawBody, $requestHeaders);
} catch (InvalidWebhook) {
    // respond 401 / 400
}

match (true) {
    $event instanceof EmailReceived => $queue->push($event->emailId),
    $event instanceof DeliveryStatusChanged => $store->recordStatus($event),
    default => null,
};

Resend redelivers webhooks on failure: handle each email id idempotently.

Storing a received email

use Jessecruz\ResendInbox\Mailbox;
use Jessecruz\ResendInbox\ResendMailbox;
use Jessecruz\ResendInbox\Threading\ThreadResolver;

$mailbox = new Mailbox('example.com', ['hello@example.com', 'support@example.com'], 'Example Team');
$resend = new ResendMailbox(\Resend::client($apiKey));

$email = $resend->receivedEmail($emailId);

if (! $mailbox->hasOwnRecipient($email->recipients())) {
    return; // addressed to another domain on the same Resend account
}

$threadId = (new ThreadResolver($lookup))->resolveFor($email); // null: start a new conversation
$address = $mailbox->mailboxFor($email->recipients());          // which of our addresses received it
$notify = ! $email->isAutoGenerated();

$lookup is your implementation of Jessecruz\ResendInbox\Threading\MessageLookup: two queries over your stored messages.

Replying

use Jessecruz\ResendInbox\Composing\Conversation;
use Jessecruz\ResendInbox\Composing\ReplyBuilder;

$composed = (new ReplyBuilder($mailbox))->build(
    from: 'support@example.com',
    to: ['maria@acme.com'],
    subject: 'Re: Pricing',
    markdown: "Hi Maria,\n\n...",
    conversation: new Conversation($storedMessageIds, $latestMessageId, $mailboxesThatReceivedMail),
);

$resendId = $resend->send($composed->email);
// store $composed->messageId, ->inReplyTo and ->references with the message

Testing

composer test
composer analyse

Contributing

Please read CONTRIBUTING.md before opening an issue or a pull request. Security problems are reported privately: see SECURITY.md.

License

The MIT License (MIT). Please see License File for more information.