agent-first-mail 0.1.0

Give your AI agent a mailbox it can actually work in — your mail pulled down into plain files it reads, triages, drafts, and files entirely on your machine, with nothing sent or changed on the real mailbox until you confirm.
Documentation
use crate::error::{AppError, Result};
use fs4::{FileExt, TryLockError};
use std::fs::{File, OpenOptions};
use std::path::Path;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LockMode {
    Shared,
    Exclusive,
}

#[derive(Debug)]
pub(crate) struct WorkspaceLock {
    file: File,
}

impl WorkspaceLock {
    pub(crate) fn acquire(root: &Path, mode: LockMode) -> Result<Self> {
        match Self::try_acquire(root, mode)? {
            Some(lock) => Ok(lock),
            None => Err(workspace_locked_error()),
        }
    }

    pub(crate) fn try_acquire(root: &Path, mode: LockMode) -> Result<Option<Self>> {
        let dir = root.join(".afmail");
        std::fs::create_dir_all(&dir).map_err(|e| AppError::io("create lock directory", &e))?;
        let path = dir.join("workspace.lock");
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .truncate(false)
            .open(&path)
            .map_err(|e| AppError::io("open workspace lock", &e))?;
        let acquired = match mode {
            LockMode::Shared => FileExt::try_lock_shared(&file),
            LockMode::Exclusive => FileExt::try_lock(&file),
        };
        if matches!(acquired, Err(TryLockError::WouldBlock)) {
            return Ok(None);
        }
        acquired.map_err(|e| AppError::io("lock workspace", &std::io::Error::from(e)))?;
        Ok(Some(Self { file }))
    }
}

fn workspace_locked_error() -> AppError {
    AppError::retryable(
        "workspace_locked",
        "another afmail command is using this workspace",
    )
    .with_hint("Wait for the running afmail command to finish, then retry this command.")
}

impl Drop for WorkspaceLock {
    fn drop(&mut self) {
        let _ = FileExt::unlock(&self.file);
    }
}