agent-first-mail 0.2.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 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);
    }
}