Skip to content

Addresses, folders, and threading

Addresses

addresses is a per-user table that maps mail addresses Lumen will accept or send from. Each user can have many addresses. Exactly one is marked as primary; deleting the primary promotes the next-oldest address.

The InitPage doesn't create any addresses — every user adds their own from Settings → Addresses after first sign-in.

Folders

Every user has the following system folders (always present, never deletable):

  • inbox — incoming mail
  • sent — sent mail
  • drafts — autosaved drafts
  • starred — virtual: messages with is_starred = 1, excluding trash & spam
  • archive — manual archive
  • spam — manual spam
  • trash — soft-delete bucket; emptied on demand

Custom folders (rows in the folders table) are addressed by their folder.id UUID rather than by name. Deleting a custom folder moves its messages back to inbox.

The "Move to" menu writes the chosen folder id directly to messages.folder.

Threading

Lumen builds a synthetic thread_id on inbound mail:

  • If the message has an In-Reply-To header, that's the thread.
  • Otherwise the first References token is used.
  • Otherwise the message's own Message-ID becomes its thread root.

Outgoing replies inherit the parent's thread_id, so a reply chain stays together in the database. The frontend currently lists messages per folder and shows them flat; GET /api/threads/:threadId returns all messages in a thread for a future thread view.

/api/messages?folder=…&q=… runs a LIKE scan against subject, preview, from_address, and to_addresses. It's not a full-text index — for larger mailboxes consider porting this to D1 FTS5 or to a separate index Worker.

Trash semantics

DELETE /api/messages/:id:

  • If the message is not in trash, it's moved to trash (soft delete).
  • If it's already in trash, the row is deleted, attachments are removed from R2, and the raw .eml blob is removed.

POST /api/messages/trash/empty does a bulk hard-delete for the current user.