PhoenixKitInbox.Messages (PhoenixKitInbox v0.2.0)

Copy Markdown View Source

Composing, sending, listing, and filing messages.

The send path

send_message/3 is one Ecto.Multi, so a message is never half-delivered:

  1. resolve every recipient string to a mailbox (unknown ones fail the whole send — silently dropping a recipient is worse than refusing)
  2. insert or update the message with status: "sent" and a sent_at stamp
  3. fan out one Delivery per recipient into their "inbox", plus the sender's own copy into "sent"

Notifications are fired after the transaction commits, by PhoenixKitInbox.Notify — a notification for a message that got rolled back would be a lie, and an outbound-email failure must never roll back a successfully delivered internal message.

Reads

Every list query is scoped to a single mailbox and folder, hitting the (mailbox_uuid, folder, inserted_at) index from V01. Nothing here reads across mailboxes — that's the whole point of the per-mailbox delivery row.

Summary

Types

A message as the UI consumes it: the delivery row (folder/seen/starred state for this mailbox) plus the message it points at.

Functions

Changeset for the compose form.

Row count for a folder, for pagination and the sidebar totals.

Discards a draft and its delivery row. Only the author's own drafts.

Fetches one message as seen by one mailbox.

Lists one folder of one mailbox, newest first.

The recipient mailboxes of a message, with their roles — the To/Cc chips in the reading pane. bcc rows are included; callers showing a message to a non-sender should filter them out.

Every message in a thread, oldest first — the conversation view.

Marks a delivery seen. Idempotent — an already-seen row keeps its original stamp.

Marks a delivery unseen.

Moves a delivery to another folder — the one operation behind Archive, Spam, Trash, and Restore in the UI.

Permanently removes a mailbox's copy of a message. The message itself and other mailboxes' copies are untouched — deleting your copy of a group thread doesn't delete anyone else's.

Saves a draft owned by mailbox, creating it on first save and updating it on subsequent ones. The draft gets exactly one delivery — into the author's own "drafts" folder.

Sends a message from mailbox.

Flips the star on a delivery.

Types

listed()

@type listed() :: %{
  delivery: PhoenixKitInbox.Schemas.Delivery.t(),
  message: PhoenixKitInbox.Schemas.Message.t()
}

A message as the UI consumes it: the delivery row (folder/seen/starred state for this mailbox) plus the message it points at.

Functions

change_message(message, attrs \\ %{})

@spec change_message(PhoenixKitInbox.Schemas.Message.t(), map()) :: Ecto.Changeset.t()

Changeset for the compose form.

count_folder(mailbox_uuid, folder, opts \\ [])

@spec count_folder(binary(), String.t(), keyword()) :: non_neg_integer()

Row count for a folder, for pagination and the sidebar totals.

delete_draft(mailbox, message_uuid)

@spec delete_draft(PhoenixKitInbox.Schemas.Mailbox.t(), binary()) ::
  :ok | {:error, :message_not_found}

Discards a draft and its delivery row. Only the author's own drafts.

fetch_for_mailbox(mailbox_uuid, message_uuid)

@spec fetch_for_mailbox(binary(), binary()) ::
  {:ok, listed()} | {:error, :message_not_found}

Fetches one message as seen by one mailbox.

Scoped to the delivery on purpose: passing a message uuid a mailbox was never sent is a {:error, :message_not_found}, not a read of someone else's mail.

list_folder(mailbox_uuid, folder, opts \\ [])

@spec list_folder(binary(), String.t(), keyword()) :: [listed()]

Lists one folder of one mailbox, newest first.

Options

  • :limit — default 50
  • :offset — default 0
  • :unseen_only — only rows with no seen_at
  • :search — case-insensitive match on subject or body

list_recipients(message_uuid)

@spec list_recipients(binary()) :: [
  %{role: String.t(), mailbox: PhoenixKitInbox.Schemas.Mailbox.t()}
]

The recipient mailboxes of a message, with their roles — the To/Cc chips in the reading pane. bcc rows are included; callers showing a message to a non-sender should filter them out.

list_thread(thread_uuid)

@spec list_thread(binary()) :: [PhoenixKitInbox.Schemas.Message.t()]

Every message in a thread, oldest first — the conversation view.

mark_seen(mailbox_uuid, message_uuid)

@spec mark_seen(binary(), binary()) ::
  {:ok, PhoenixKitInbox.Schemas.Delivery.t()} | {:error, :message_not_found}

Marks a delivery seen. Idempotent — an already-seen row keeps its original stamp.

mark_unseen(mailbox_uuid, message_uuid)

@spec mark_unseen(binary(), binary()) ::
  {:ok, PhoenixKitInbox.Schemas.Delivery.t()} | {:error, :message_not_found}

Marks a delivery unseen.

move_to_folder(mailbox_uuid, message_uuid, folder)

@spec move_to_folder(binary(), binary(), String.t()) ::
  {:ok, PhoenixKitInbox.Schemas.Delivery.t()}
  | {:error, :message_not_found | Ecto.Changeset.t()}

Moves a delivery to another folder — the one operation behind Archive, Spam, Trash, and Restore in the UI.

purge(mailbox_uuid, message_uuid)

@spec purge(binary(), binary()) :: :ok | {:error, :message_not_found}

Permanently removes a mailbox's copy of a message. The message itself and other mailboxes' copies are untouched — deleting your copy of a group thread doesn't delete anyone else's.

save_draft(mailbox, sender_user_uuid, attrs)

Saves a draft owned by mailbox, creating it on first save and updating it on subsequent ones. The draft gets exactly one delivery — into the author's own "drafts" folder.

Recipients are not resolved here. A half-typed address is a normal state for a draft; validation happens at send.

send_message(mailbox, sender_user_uuid, attrs)

@spec send_message(PhoenixKitInbox.Schemas.Mailbox.t(), binary(), map()) ::
  {:ok, PhoenixKitInbox.Schemas.Message.t()} | {:error, term()}

Sends a message from mailbox.

attrs keys:

  • "to" / "cc" / "bcc" — comma-separated recipient strings, or lists. Each entry is a mailbox slug or address (see Mailboxes.fetch_mailbox_by_recipient/1).
  • "subject", "body", "body_format"
  • "uuid" — promotes an existing draft instead of creating a new message
  • "parent_uuid" — set when replying; the thread is inherited from it

Returns {:error, {:unknown_recipients, ["typo@example.com"]}} when an address doesn't resolve — the whole send is refused so the sender finds out immediately rather than discovering a missing recipient later.

toggle_star(mailbox_uuid, message_uuid)

@spec toggle_star(binary(), binary()) ::
  {:ok, PhoenixKitInbox.Schemas.Delivery.t()} | {:error, :message_not_found}

Flips the star on a delivery.