Postal integration for Laravel
Inbound mail
Inbound mail
Postal routes can deliver received mail straight to your app over HTTP.
The package receives, verifies, deduplicates and turns each message into a
PostalInboundMessage event.
1. Point a route at your app
Postal admin → your mail server → Routes → add a route (e.g.
[email protected] or a wildcard) with an HTTP Endpoint delivering
to:
https://your-app.example/postal/inbound/{server-name}
Choose the BodyAsJSON encoding (recommended; FormData also works) and either format:
- Hash — parsed fields (subject, bodies, headers, attachments).
- RawMessage — the full RFC 2822 message, exposed via
rawMessage().
Deliveries are signed with the same install-wide key as webhooks
(POSTAL_WEBHOOK_PUBLIC_KEY — fetch with postal:webhook-key), and
verification fails closed.
2. Listen
use Cbox\LaravelPostal\Events\PostalInboundMessage;
Event::listen(function (PostalInboundMessage $event) {
$message = $event->payload; // InboundMessage DTO
$message->rcptTo; // envelope recipient
$message->mailFrom; // envelope sender
$message->subject;
$message->plainBody;
$message->htmlBody;
foreach ($message->attachments as $attachment) {
Storage::put($attachment->filename, $attachment->content());
}
if ($message->isRaw()) {
$message->rawMessage(); // full RFC 2822 source
}
});
3. Semantics worth knowing
- Ack fast. The controller queues
ProcessInboundMessageand answers 200 immediately. Configure isolation withPOSTAL_INBOUND_QUEUE/POSTAL_INBOUND_CONNECTION. - Your response drives Postal's route. 2xx marks the message delivered; 5xx makes Postal retry; other 4xx (including a signature rejection) hard-fails and may bounce — so a misconfigured public key stops inbound flow visibly rather than silently.
- Idempotent. Redeliveries deduplicate on the Postal message id before
any side effect, atomically — a failure mid-processing rolls the dedupe
marker back so the retry is clean. Every
PostalInboundMessageexposes the same key viauuid()for listener-side dedupe. - Mind your queue driver's payload limit. The queued job carries the
full delivery body (attachments arrive base64-encoded). SQS caps
payloads at 256 KB — for attachment-heavy mail use Redis/database
queues, or configure the Postal route without attachments and fetch
bodies via
Postal::message()instead. Attachment bytes are decoded lazily ($attachment->content()), so metadata-only listeners never pay for them. - Stored. With the store enabled, each inbound message gets a
postal_messagesrow (direction = incoming, statusReceived) and anInboundMessageentry in the event log —postal:tailshows inbound traffic live. Attachment data and raw source are deliberately kept out of the log; persist those in your listener if you need them.