Logotipo Hedhog

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

FieldTypeRequiredNotes
accountIdintyesSending mailbox; caller must have access
threadIdintnoReply within an existing thread; omit to start a new thread
toRecipientDto[] ({ email, name? })yesAt least 1 recipient required
ccRecipientDto[] ({ email, name? })no
bccRecipientDto[] ({ email, name? })no
replyToRecipientDto[] ({ email, name? })no
subjectstring (max 255)yes
textBodystringno
htmlBodystringno