Logotipo Hedhog

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

FieldTypeRequiredNotes
emailstring (email, max 255)yesNormalized to lowercase; must be globally unique among active accounts
displayNamestring (max 180)yes
providerDomainIdstring (max 255)no
providerRegionstring (max 127)no
isDefaultbooleannoUnsets is_default on the caller's other accounts when true
isActivebooleannoDefault: true
signatureHtmlstringno
usersAccountMemberDto[] ({ userId, role? })noAdditional 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)