Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
inbox Submodule
messages
Send and read individual inbox_message records — outbound send queueing via the Queue library, delivery via Resend, and the raw provider payload for debugging.
Source path: libraries/inbox/src/message
Module file: message.module.ts
Introduction
The Messages submodule (`MessageController`, mounted at `/inbox/messages`) is the read and compose surface over `inbox_message`, the row-per-email table that both inbound and outbound mail lives in. Listing and fetching are always scoped through `InboxAccessService.listAccessibleAccountIds`, and `GET /inbox/messages/:id/raw-payload` exposes the message's stored `raw_payload` JSON — for an outbound message this is the queue job metadata (`queueJobId`, `queueName`, `queueType`); for an inbound message it is the full original webhook body from Resend — which is useful for support/debugging without re-triggering delivery.
Sending is asynchronous by design. `POST /inbox/messages/send` never talks to Resend directly: `MessageService.send` validates the caller has access to the target account, normalizes the subject (stripping leading `re:`/`fwd:` prefixes for threading), builds a preview string, resolves the account's `from_email`/`from_name`, and then — inside a single transaction — either creates a new `inbox_thread` or reuses an existing one (`threadId` in the request body), creates the `inbox_message` row with `status: queued`, fans out `to`/`cc`/`bcc`/`replyTo` into `inbox_recipient` rows, and writes a `delivery_delayed` `mail_event`. Only after that transaction commits does it enqueue a job (`type: inbox.email.send`, queue `inbox-emails`, `maxAttempts: 3`) through `QueueJobService`, recording the returned `queueJobId` back onto the message's `raw_payload`. If enqueueing itself throws, the message is immediately marked `failed` with a synthetic system-reply message explaining the failure, and the endpoint responds with a 400 rather than leaving a message stuck in `queued` with no job behind it.
Actual delivery happens out-of-band: `MessageSendWorkerService` registers itself with `QueueHandlerRegistry` under the `inbox.email.send` job type and, when the queue invokes it, calls `MessageService.processQueuedSend`, which loads the message's recipients, calls `ResendIntegrationService.sendEmail`, and on success updates the message to `status: sent` with the Resend-assigned `provider_message_id`, writes a `sent` `mail_event`, and bumps the thread's `last_message_at`. If the worker exhausts all `maxAttempts` retries, `MessageSendWorkerService` calls `MessageService.handleQueuedSendFailure`, which marks the original message `failed` and — idempotently, guarded by a check for an existing `queue-failed:{id}` mail_event — synthesizes an inbound "delivery failure" message from `[email protected]` back into the same thread, in Portuguese, so the sender sees the failure the same way they would see any other reply, without needing a separate notifications UI.
This controller file also declares a second, unrelated controller — `InboxWebhookController` — documented separately under Webhooks; it shares this module's wiring (`message.module.ts`) but has no auth relationship to the message CRUD endpoints above.
HTTP Endpoints
4 endpoints
Paginated list of messages across the caller's accessible accounts, each with its recipients and latest mail_event.
- Query
- page, pageSize, search, accountId?, threadId?
Get full detail for a single message, including all recipients and its complete mail_event history.
- Params
- id (int)
Get the message's stored raw_payload JSON — queue job metadata for outbound messages, or the original Resend webhook body for inbound messages.
- Params
- id (int)
Create and enqueue an outbound message for asynchronous delivery via the Queue library. Creates the thread/message/recipients synchronously (status: queued); actual delivery through Resend happens in a background worker job.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| accountId | int | yes | Sending mailbox; caller must have access |
| threadId | int | no | Reply within an existing thread; omit to start a new thread |
| to | RecipientDto[] ({ email, name? }) | yes | At least 1 recipient required |
| cc | RecipientDto[] ({ email, name? }) | no | — |
| bcc | RecipientDto[] ({ email, name? }) | no | — |
| replyTo | RecipientDto[] ({ email, name? }) | no | — |
| subject | string (max 255) | yes | — |
| textBody | string | no | — |
| htmlBody | string | no | — |