Skip to main content

MailboxEngine

Struct MailboxEngine 

Source
pub struct MailboxEngine<S> { /* private fields */ }
Expand description

The mailbox engine. Generic over its MailStore so the same logic runs against crate::InMemoryStore in tests and, later, a persistent store – neither of which this crate needs to know about here.

Implementations§

Source§

impl<S: MailStore> MailboxEngine<S>

Source

pub fn new(store: S) -> Self

Source

pub fn authenticate( &self, presented_secret: &str, ) -> Result<ParticipantId, MailError>

Authenticates a presented secret and derives the caller’s identity from the match – never from anything the caller states. This is the one place a ParticipantId is allowed to enter the engine from outside; every other method takes an already-authenticated identity as an argument (see mail4agent/CLAUDE.md’s “a sender is never a field the caller fills in”).

Authenticates the account only. Which session, if any, is calling is a separate fact the caller establishes by kernel attestation (mail4agent-attest, outside this crate) and hands to Self::ensure_session – this method has no notion of a session at all.

Participants are indexed by the SHA-256 digest of their secret and looked up by that exact digest, not scanned. That lookup is safe to do by direct index rather than in constant time because the secret is 32 random bytes: 256 bits of entropy an attacker cannot already be close to guessing, so nothing the lookup structure’s timing could reveal (which bucket, how many probes) narrows the search in any useful way. The digest found is then confirmed against the presented one with a constant-time compare (subtle) before being trusted – redundant with an exact map lookup by construction, but it costs nothing and removes any dependency on the map’s own equality/hashing behaviour for the actual authentication decision.

Source

pub fn register_participant( &mut self, id: ParticipantId, label: Option<String>, permissions: ParticipantPermissions, ) -> Result<String, MailError>

Registers a new participant and returns its secret. The secret is generated here, handed back once, and never stored – only its digest is kept, in ParticipantRecord::secret_digest.

Source

pub fn deregister_participant( &mut self, id: &ParticipantId, ) -> Result<(), MailError>

Removes a participant’s registration. Its secret stops authenticating immediately; stale entries in a room’s member set naming this id are inert (see MailStore::deregister_participant’s doc comment).

Source

pub fn rotate_participant_secret( &mut self, id: &ParticipantId, ) -> Result<String, MailError>

Issues a fresh secret for an existing participant, invalidating the old one. Shares its mechanism with Self::revoke_participant_secret – revoking is rotating and discarding the new secret rather than returning it.

Source

pub fn revoke_participant_secret( &mut self, id: &ParticipantId, ) -> Result<(), MailError>

Invalidates a participant’s current secret without issuing a new one usable by anyone: it is replaced by the digest of a fresh secret that is generated and immediately discarded, so no plaintext maps to it. The participant must call Self::rotate_participant_secret (or be re-registered) to authenticate again.

Source

pub fn set_listener( &mut self, id: &ParticipantId, url: String, ) -> Result<(), MailError>

Registers (or replaces) the URL the mailbox POSTs a mail4agent_api::DeliveryNotification to whenever mail arrives for id or any of its sessions. Operator-only at the daemon’s own admin surface (POST /admin/listener); this method itself only enforces that id is a real, already-registered participant and that url is a well-formed loopback URL – see [validate_listener_url] for exactly what that means and why.

Source

pub fn remove_listener(&mut self, id: &ParticipantId) -> Result<(), MailError>

Removes id’s registered delivery listener, if any. Idempotent: removing an account with no listener registered is not an error.

Source

pub fn create_room( &mut self, id: RoomId, now_unix_ms: u64, ) -> Result<(), MailError>

Creates a room with no members. now_unix_ms is threaded through by the caller (not read from a clock here) so the engine stays a pure function of its inputs – the same discipline Self::send and Self::ack follow.

Source

pub fn add_room_member( &mut self, room: &RoomId, participant: ParticipantId, ) -> Result<(), MailError>

Adds participant to room. Idempotent: adding an existing member is not an error. Membership is always an account’s: every session of participant inherits it (see Self::resolve_identity).

Source

pub fn remove_room_member( &mut self, room: &RoomId, participant: &ParticipantId, ) -> Result<(), MailError>

Removes participant from room. Idempotent: removing a non-member is not an error. Readability of a room is present-tense (see Self::is_readable): once removed, the participant’s next Self::inbox call shows none of that room’s mail at all, past or future – there is no partial history left behind for a former member.

Source

pub fn ensure_session( &mut self, account: ParticipantId, session_id: SessionId, card: SessionCard, now_unix_ms: u64, ) -> Result<SessionId, MailError>

Registers a session, or refreshes an already-registered one – idempotent, and the only way a session enters the mailbox at all. This replaces enrolment as a separate step (mailbox-service-extraction-and-signed-session-identity-2026-09-16.md §5e): the first call for a given session_id creates it under account; every later call for the same id refreshes last_seen and card’s attested/corroborated groups.

Never touches card.declared. Self::set_declared is the only way that group is ever written – so this preserves whatever is already on file (empty, the first time) regardless of what the caller passed in card.declared. A caller that wants to declare something calls Self::set_declared itself; passing it here would let attestation traffic silently overwrite what a session said about itself.

Refuses MailError::SessionAccountMismatch if session_id is already on file under a different account: a session’s account cannot change out from under it, only be created once.

Source

pub fn set_declared( &mut self, session: &SessionId, working_on: Option<String>, role: Option<String>, parent: Option<SessionId>, ) -> Result<(), MailError>

Sets session’s declared group – what it is working on, its role, which session spawned it. The only way that group is ever written; Self::ensure_session never touches it (see that method’s own doc comment). Refuses MailError::UnknownSession if session has never been through Self::ensure_session.

Source

pub fn send( &mut self, sender: &Address, request: SendRequest, now_unix_ms: u64, ) -> Result<SendResponse, MailError>

Sends a message. sender must already be the address Self::authenticate (plus, for a session, Self::ensure_session) established – never a field read out of request; SendRequest has no from, and must never grow one (mail4agent/CLAUDE.md). sender is the session’s own address when a session sends, the account’s when an account does – see Address.

Sending to a room never requires membership; reading one does. This mirrors the mailbox being ported: anyone could write to a task forum, but only those who could see the task could read it (see mailbox-service-extraction-and-signed-session-identity-2026-09-16.md §5b). It is a deliberate parity choice, not an oversight, and it is worth revisiting once this crate has its own callers: a mailbox that lets any registered participant write into a room it cannot itself read is a wider write surface than most groupware would choose.

A message’s id is derived from its content and, when present, from SendRequest::idempotency_key (see [derive_message_id]); a repeat send carrying the same key from the same sender address returns the original SendResponse and creates nothing, checked and recorded atomically by MailStore::insert_message. Without a key, a repeat send is a second message – correct, because sending the same text twice on purpose should produce two messages.

Source

pub fn inbox( &self, reader: &Address, since_unix_ms: u64, limit: u16, ) -> Result<InboxPage, MailError>

Returns a page of reader’s own inbox: messages addressed directly to reader (its own session address if reader is a session, plus its account’s direct mail – see Self::own_messages), plus messages to any room its account is currently a member of (membership is evaluated now, not at send time), no older than since_unix_ms, ordered by created_at_unix_ms then message_id, truncated to limit. unread counts every currently-readable message with no ack on file for reader, independent of since and limit.

This never widens for an operator. “An operator may read any address” means any address it names, one at a time – see Self::inbox_of, the door through which an operator reaches someone else’s inbox. An operator calling this method sees only its own mail, exactly like anyone else.

Source

pub fn inbox_of( &self, caller: &Address, target: &Address, since_unix_ms: u64, limit: u16, ) -> Result<InboxPage, MailError>

Returns target’s inbox by exactly Self::inbox’s own rule – never widened, regardless of who is asking. Requires caller.operator or caller == target; refuses PermissionDenied { need: "mail:operator" } otherwise.

This is the operator door onto a named address, one at a time – not a firehose over the whole mailbox. target may name an account or one specific session of it. When caller == target this is exactly Self::inbox under another name (and still requires target’s own may_read); when an operator names someone else, the target’s own may_read is not consulted, because the authorization has already been established by the operator bit.

Source

pub fn ack( &mut self, reader: &Address, message_id: &MessageId, now_unix_ms: u64, ) -> Result<Ack, MailError>

Records reader’s acknowledgement of message_id. Refuses NotAddressedToYou unless reader may read the message (see Self::is_readable, which keeps its operator override: a named single message is a different thing from a bulk inbox listing). Idempotent on (message_id, reader): a second ack of the same message by the same reader address returns the first ack unchanged rather than overwriting its timestamp.

now_unix_ms is threaded through by the caller for the same reason Self::send takes it: the engine reads no clock of its own.

Source

pub fn message_get( &self, reader: &Address, message_id: &MessageId, ) -> Result<Message, MailError>

Fetches one message by id. Refuses UnknownMessage if no such message exists, NotAddressedToYou if it exists but reader may not read it (see Self::is_readable, which keeps its operator override for the same reason Self::ack does).

Source

pub fn unread_count_of( &self, caller: &Address, target: &Address, ) -> Result<UnreadCount, MailError>

Returns how many currently-readable messages target has not yet acked – the same figure InboxPage::unread carries for the same address. Requires caller.operator or caller == target, the same authorization Self::inbox_of uses, enforced here rather than left to a wire layer that could forget it.

Source

pub fn directory( &self, caller: &Address, is_alive: LivenessCheck<'_>, ) -> Result<Directory, MailError>

Returns the mailbox’s own directory: every registered account (id and label; never a secret digest or a permission bit – see crate::ParticipantSummary), each with its own live sessions nested under it, and every room the mailbox tracks, each marked with whether caller’s account currently belongs to it.

is_alive is given, never performed here: this crate learns nothing about processes (see LivenessCheck). It is called once per listed session, with that session’s own (pid, started_at_unix_ms), to fill SessionEntry::live.

Gated on caller.may_read, the same capability Self::inbox and Self::message_get require: seeing who else exists is a read of the mailbox, not a distinct capability. A participant is visible to every other participant that may read at all, with no exception for a listed participant’s own permission bits – knowing someone exists is not the capability that matters (reading their mail is, and that is unaffected by this), so gating the directory’s completeness on each target’s may_read/may_send would only make it an unreliable directory for no privacy this mailbox actually provides.

Auto Trait Implementations§

§

impl<S> Freeze for MailboxEngine<S>
where S: Freeze,

§

impl<S> RefUnwindSafe for MailboxEngine<S>
where S: RefUnwindSafe,

§

impl<S> Send for MailboxEngine<S>
where S: Send,

§

impl<S> Sync for MailboxEngine<S>
where S: Sync,

§

impl<S> Unpin for MailboxEngine<S>
where S: Unpin,

§

impl<S> UnsafeUnpin for MailboxEngine<S>
where S: UnsafeUnpin,

§

impl<S> UnwindSafe for MailboxEngine<S>
where S: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more