agent-first-mail 0.3.0

Let your AI agent work your inbox — email pulled into plain files it reads, sorts, and drafts on your machine, with nothing sent until you confirm.
Documentation
use super::*;

impl Workspace {
    pub(crate) fn record_pending_push_item(&self, item: &PushItem) -> Result<()> {
        let pending = WorkspacePendingPush {
            push_id: item.push_id.clone(),
            kind: item.display_kind(),
            queued_rfc3339: item.created_rfc3339.clone(),
            last_error: item.last_error.clone(),
        };
        for message_id in item.message_ids() {
            self.update_message_push_state(message_id, |state| {
                state
                    .pending
                    .retain(|entry| entry.push_id != pending.push_id);
                state.pending.push(pending.clone());
                state.pending.sort_by(|a, b| a.push_id.cmp(&b.push_id));
            })?;
        }
        Ok(())
    }

    pub(crate) fn clear_pending_push_item(&self, item: &PushItem) -> Result<()> {
        let push_ids = [item.push_id.clone()];
        let message_ids = item.message_ids();
        for message_id in message_ids {
            self.clear_message_pending_pushes(message_id, &push_ids, true)?;
        }
        if !message_ids.is_empty() {
            self.refresh_disposition_views()?;
        }
        Ok(())
    }

    pub(crate) fn mark_pending_push_error(&self, item: &PushItem, error: &str) -> Result<()> {
        let message_ids = item.message_ids();
        for message_id in message_ids {
            self.update_message_push_state(message_id, |state| {
                for pending in &mut state.pending {
                    if pending.push_id == item.push_id {
                        pending.last_error = Some(error.to_string());
                    }
                }
            })?;
        }
        if !message_ids.is_empty() {
            self.refresh_disposition_views()?;
        }
        Ok(())
    }

    pub(super) fn update_message_push_state(
        &self,
        message_id: &str,
        update: impl FnOnce(&mut WorkspacePushState),
    ) -> Result<()> {
        validate_id("message_id", message_id)?;
        let mut message = self.read_message_by_id(message_id)?;
        let mut state = message.workspace.push.unwrap_or_default();
        update(&mut state);
        if state.pending.is_empty() && state.last_completed_rfc3339.is_none() {
            message.workspace.push = None;
        } else {
            message.workspace.push = Some(state);
        }
        self.write_message_materialized_cache(&message)
    }

    pub(super) fn clear_message_pending_pushes(
        &self,
        message_id: &str,
        push_ids: &[String],
        completed: bool,
    ) -> Result<()> {
        if push_ids.is_empty() {
            return Ok(());
        }
        let remove = push_ids.iter().cloned().collect::<BTreeSet<_>>();
        self.update_message_push_state(message_id, |state| {
            let before = state.pending.len();
            state
                .pending
                .retain(|pending| !remove.contains(&pending.push_id));
            if completed && (state.pending.len() != before || before == 0) {
                state.last_completed_rfc3339 = Some(now_rfc3339());
            }
        })
    }
}