Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
inbox Submodule
accounts
Manages the inbox mailboxes (inbox_account) that threads, messages, drafts and labels all belong to, including account ownership and shared-member access.
Source path: libraries/inbox/src/account
Module file: account.module.ts
Introduction
The Accounts submodule owns `inbox_account`, the root entity every other Inbox table hangs off of via `account_id`. Each account represents a single mailbox on the Resend provider (the `provider` column is an enum currently hardcoded to the single value `RESEND`), identified by a unique, lowercase-normalized `email` and a human-readable `display_name`. Optional `provider_domain_id` / `provider_region` fields let the account reference a specific verified sending domain when the platform manages multiple Resend domains, and `signature_html` stores the HTML signature appended when composing from that account.
Account access is not purely single-owner: `inbox_account_user` is a join table with a `role` of `owner` or `member`, so a mailbox can be shared with several users. `AccountService.create` always upserts the creating user as `owner` and then upserts any additional `users` passed in the request body (deduplicated by `userId`) with their requested role, defaulting to `member`. `AccountService.update` treats the `users` array as authoritative when present: it deletes all existing membership rows for the account and recreates them from scratch, always keeping the caller as `owner` regardless of what was passed, which prevents a member from accidentally removing their own access via a partial update.
Only one account per user can be the default at a time. Setting `isDefault: true` on create or update runs inside a transaction that first flips `is_default` to `false` on every other account owned by that `user_id`, then applies the new default — this is a per-owner constraint (mirrored by the `user_id, is_default` unique partial index in `inbox_account.yaml`), not a global one, so two different users can each have their own default account. Deleting an account is a soft delete: `deleted_at` is stamped, and `is_active`/`is_default` are both forced to `false` so a removed mailbox immediately drops out of default-account resolution and active-account filters without needing a second write.
Access control here is intentionally different from the rest of the module. `InboxAccessService.assertAccountAccess` (used by update/delete) checks that the caller is either the account's original creator, a row in `inbox_account_user`, or holds the `admin`/`admin-inbox` role — but `AccountService.list` performs no such per-user filtering at all: it returns every non-deleted account matching the search term, relying entirely on the route-level role gate (`admin`, `admin-inbox`, or `inbox-owner`) to restrict who can call the endpoint. In other words, any user holding one of those three roles sees the full account directory, not just accounts they belong to — unlike Threads, Messages, Drafts and Labels, which all scope their queries through `InboxAccessService.listAccessibleAccountIds(userId)` and only ever return records the caller can see. Creating and updating an account's duplicate-email check (`ConflictException` on collision) also runs case-insensitively after trimming, so `[email protected]` and `[email protected]` are treated as the same mailbox.
HTTP Endpoints
4 endpoints
Paginated list of inbox accounts (ordered default-first, then most recent), with each account's shared members (id, role, name, photoId). Not scoped to the caller — any user with one of the three allowed roles sees every non-deleted account.
- Query
- page, pageSize, search
Create a new inbox account (mailbox). Provider is hardcoded to RESEND.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| string (email, max 255) | yes | Normalized to lowercase; must be globally unique among active accounts | |
| displayName | string (max 180) | yes | — |
| providerDomainId | string (max 255) | no | — |
| providerRegion | string (max 127) | no | — |
| isDefault | boolean | no | Unsets is_default on the caller's other accounts when true |
| isActive | boolean | no | Default: true |
| signatureHtml | string | no | — |
| users | AccountMemberDto[] ({ userId, role? }) | no | Additional shared members; creator is always upserted as owner |
Update an inbox account (partial update, same fields as create). If "users" is provided, it fully replaces the account's membership list (the caller is always kept as owner).
- Params
- id (int)
Soft-delete an inbox account (sets deleted_at, is_active: false, is_default: false). Does not cascade-delete threads/messages.
- Params
- id (int)