From aee4bc2eaaa61742240ccf27329dca868accb4b9 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 16 Mar 2026 14:48:53 +0200 Subject: [PATCH] fix: path issues on linux --- CLAUDE.md | 121 ++++++++++++++++++++++ crates/cull-gmail/src/cli/init_cli.rs | 2 +- crates/cull-gmail/src/cli/messages_cli.rs | 10 +- crates/cull-gmail/src/client_config.rs | 8 +- crates/cull-gmail/src/message_list.rs | 12 ++- crates/cull-gmail/src/rule_processor.rs | 14 ++- 6 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d197832 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`cull-gmail` is a Rust-based Gmail management library and CLI tool that enables automated email culling operations through the Gmail API. It supports message querying, filtering, batch operations (trash/delete), and rule-based retention policies. + +## Development Commands + +```bash +# Build the project +cargo build + +# Run all tests +cargo test + +# Run specific test file +cargo test --test gmail_client_unit_tests + +# Run ignored integration tests (Gmail API integration) +cargo test --test gmail_message_list_integration -- --ignored + +# Format code +cargo fmt + +# Run linter +cargo clippy + +# Check for security vulnerabilities +cargo audit +``` + +## Architecture + +### Workspace Structure + +``` +cull-gmail/ +├── crates/cull-gmail/ # Library crate (public API) +│ ├── src/ +│ │ ├── cli/ # CLI command implementations +│ │ │ ├── main.rs # CLI entry point +│ │ │ ├── labels_cli.rs # labels subcommand +│ │ │ ├── messages_cli.rs # messages subcommand +│ │ │ ├── rules_cli.rs # rules subcommand +│ │ │ ├── init_cli.rs # init subcommand +│ │ │ └── token_cli.rs # token subcommand +│ │ ├── client_config.rs # OAuth2 configuration +│ │ ├── gmail_client.rs # Gmail API client +│ │ ├── message_list.rs # Message list management +│ │ ├── rules.rs # Rules configuration +│ │ ├── retention.rs # Retention policy definitions +│ │ ├── rule_processor.rs # Rule execution logic +│ │ ├── eol_action.rs # End-of-life actions (trash/delete) +│ │ ├── error.rs # Error types +│ │ └── utils.rs # Utility functions +│ └── tests/ # Test files +├── docs/ # Documentation +└── .circleci/ # CI configuration +``` + +### Key Components + +**GmailClient** (`gmail_client.rs`): Main client for Gmail API interactions. Handles authentication, message querying, and batch operations. + +**Rules** (`rules.rs`): Configuration for automated retention policies with validation and execution. + +**Retention** (`retention.rs`): Defines message age-based retention policies (e.g., "older_than:1y"). + +**RuleProcessor** (`rule_processor.rs`): Executes rules against message lists, applying appropriate actions. + +**ClientConfig** (`client_config.rs`): Manages OAuth2 credentials and configuration file parsing. + +**CLI** (`cli/`): Command-line interface with clap-based argument parsing. + +## Gmail API Integration + +- Uses `google-gmail1` crate (version 7.0.0) with `yup-oauth2` and `aws-lc-rs` features +- OAuth2 authentication with token caching in `~/.cull-gmail/gmail1/` +- Default page size: 200 messages (configurable via `DEFAULT_MAX_RESULTS`) +- Supports Gmail search syntax for queries + +## Testing + +- Unit tests: `cargo test` +- Integration tests: Located in `crates/cull-gmail/tests/` +- Gmail API integration tests are ignored by default (`#[ignore]`) and require `--ignored` flag +- Mock tests available in test suite for unit testing + +## Configuration + +- Default config path: `~/.cull-gmail/cull-gmail.toml` +- OAuth2 credential file path: `~/.cull-gmail/client_secret.json` +- Rules file: `~/.cull-gmail/rules.toml` +- Environment variables override config settings (e.g., `APP_CREDENTIAL_FILE`) + +## Error Handling + +The library uses a custom `Error` type with variants for: +- `NoLabelsFound`: Mailbox has no labels +- `LabelNotFoundInMailbox(String)`: Specific label not found +- `RuleNotFound(usize)`: Rule ID doesn't exist +- `GoogleGmail1(Box)`: Gmail API errors +- `StdIO(std::io::Error)`: File I/O errors +- `Config(config::ConfigError)`: Configuration errors + +## Logging + +- Uses `log` crate with `env_logger` +- Verbosity controlled via CLI flags (`-v`, `-vv`, `-vvv`) and `RUST_LOG` environment variable +- Log levels: error, warn, info, debug, trace + +## Code Style + +- Rust edition 2024 +- Minimum Rust version: 1.88 +- Clippy lints configured in workspace +- Follows official Rust style guide +- Use `cargo fmt` for formatting +- Use `cargo clippy` for linting \ No newline at end of file diff --git a/crates/cull-gmail/src/cli/init_cli.rs b/crates/cull-gmail/src/cli/init_cli.rs index 6ae05e0..5def24b 100644 --- a/crates/cull-gmail/src/cli/init_cli.rs +++ b/crates/cull-gmail/src/cli/init_cli.rs @@ -1005,8 +1005,8 @@ impl InitCli { let config_path = parse_config_root(config_root); let client_config = ClientConfig::builder() - .with_credential_file(InitDefaults::credential_filename()) .with_config_path(config_path.to_string_lossy().as_ref()) + .with_credential_file(InitDefaults::credential_filename()) .build(); // Initialize Gmail client which will trigger OAuth flow if needed diff --git a/crates/cull-gmail/src/cli/messages_cli.rs b/crates/cull-gmail/src/cli/messages_cli.rs index 83576cb..a9447b9 100644 --- a/crates/cull-gmail/src/cli/messages_cli.rs +++ b/crates/cull-gmail/src/cli/messages_cli.rs @@ -289,17 +289,13 @@ impl MessagesCli { match self.action { MessageAction::List => { - if log::max_level() >= log::Level::Info { - client.log_messages("", "").await - } else { - Ok(()) - } + let messages = client.messages(); + println!("Found {} messages matching query", messages.len()); + Ok(()) } MessageAction::Trash => client.batch_trash().await, MessageAction::Delete => client.batch_delete().await, } - - // Ok(()) } /// Configures the Gmail client with filtering and pagination parameters. diff --git a/crates/cull-gmail/src/client_config.rs b/crates/cull-gmail/src/client_config.rs index 5dbcbc5..22057a6 100644 --- a/crates/cull-gmail/src/client_config.rs +++ b/crates/cull-gmail/src/client_config.rs @@ -333,7 +333,7 @@ impl ClientConfig { console.installed.unwrap() }; - let persist_path = format!("{}/gmail1", config_root.full_path().display()); + let persist_path = format!("{}/gmail1/tokencache.json", config_root.full_path().display()); Ok(ClientConfig { config_root, @@ -605,7 +605,7 @@ impl ConfigBuilder { } pub fn build(&self) -> ClientConfig { - let persist_path = format!("{}/gmail1", self.full_path()); + let persist_path = format!("{}/gmail1/tokencache.json", self.full_path()); ClientConfig { secret: self.secret.clone(), @@ -889,7 +889,7 @@ mod tests { .with_config_path("/custom/path") .build(); - assert_eq!(config.persist_path(), "/custom/path/gmail1"); + assert_eq!(config.persist_path(), "/custom/path/gmail1/tokencache.json"); } #[test] @@ -906,7 +906,7 @@ mod tests { assert_eq!(secret.client_secret, "accessor-test-secret"); // Test persist_path() accessor - assert_eq!(config.persist_path(), "/test/path/gmail1"); + assert_eq!(config.persist_path(), "/test/path/gmail1/tokencache.json"); // Test full_path() accessor assert_eq!(config.full_path(), "/test/path"); diff --git a/crates/cull-gmail/src/message_list.rs b/crates/cull-gmail/src/message_list.rs index fd84166..7d1b497 100644 --- a/crates/cull-gmail/src/message_list.rs +++ b/crates/cull-gmail/src/message_list.rs @@ -405,7 +405,9 @@ impl GmailService for GmailClient { if let Some(token) = page_token { call = call.page_token(&token); } + eprintln!("[DEBUG] Making Gmail API call: messages_list(query='{}', max_results={})", query, max_results); let (_response, list) = call.doit().await.map_err(Box::new)?; + eprintln!("[DEBUG] API call completed"); Ok(list) } @@ -483,7 +485,12 @@ impl MessageList for GmailClient { /// Run the Gmail api as configured async fn get_messages(&mut self, pages: u32) -> Result<()> { + eprintln!("[DEBUG] Starting message fetch: pages={}, query='{}', labels={:?}", + pages, self.query, self.label_ids); let list = self.list_messages(None).await?; + eprintln!("[DEBUG] Got response: {} messages (estimated: {})", + list.messages.as_ref().map(|m| m.len()).unwrap_or(0), + list.result_size_estimate.unwrap_or(0)); match pages { 1 => {} 0 => { @@ -553,6 +560,7 @@ impl MessageList for GmailClient { } async fn log_messages(&mut self, pre: &str, post: &str) -> Result<()> { + eprintln!("[INFO] Processing {} messages for output...", self.messages.len()); for i in 0..self.messages.len() { let id = self.messages[i].id().to_string(); log::trace!("{id}"); @@ -575,7 +583,9 @@ impl MessageList for GmailClient { continue; } } - log::info!("{pre}{}{post}", message.list_date_and_subject()); + let output = format!("{pre}{}{post}", message.list_date_and_subject()); + println!("{}", output); + log::info!("{}", output); } Ok(()) diff --git a/crates/cull-gmail/src/rule_processor.rs b/crates/cull-gmail/src/rule_processor.rs index a81464d..fb8b6fa 100644 --- a/crates/cull-gmail/src/rule_processor.rs +++ b/crates/cull-gmail/src/rule_processor.rs @@ -481,12 +481,13 @@ impl RuleProcessor for GmailClient { return Ok(()); } - self.log_messages("Message with subject `", "` permanently deleted") - .await?; + eprintln!("[INFO] Deleting {} messages using batch delete API...", message_ids.len()); + log::info!("Permanently deleting {} messages using batch API", message_ids.len()); self.process_in_chunks(message_ids, EolAction::Delete) .await?; + eprintln!("[INFO] Batch delete completed"); Ok(()) } /// Moves all prepared messages to Gmail's trash folder using batch modify API. @@ -512,12 +513,13 @@ impl RuleProcessor for GmailClient { return Ok(()); } - self.log_messages("Message with subject `", "` moved to trash") - .await?; + eprintln!("[INFO] Moving {} messages to trash using batch modify API...", message_ids.len()); + log::info!("Moving {} messages to trash using batch API", message_ids.len()); self.process_in_chunks(message_ids, EolAction::Trash) .await?; + eprintln!("[INFO] Batch trash completed"); Ok(()) } @@ -550,6 +552,7 @@ impl RuleProcessor for GmailClient { } async fn call_batch_delete(&self, ids: &[String]) -> Result<()> { + let count = ids.len(); let ids = Some(Vec::from(ids)); let batch_request = BatchDeleteMessagesRequest { ids }; log::trace!("{batch_request:#?}"); @@ -566,11 +569,13 @@ impl RuleProcessor for GmailClient { log::trace!("Batch delete response {res:?}"); res?; + eprintln!("[DEBUG] Batch delete completed: {} messages", count); Ok(()) } async fn call_batch_trash(&self, ids: &[String]) -> Result<()> { + let count = ids.len(); let ids = Some(Vec::from(ids)); let add_label_ids = Some(vec![TRASH_LABEL.to_string()]); let remove_label_ids = Some(vec![INBOX_LABEL.to_string()]); @@ -592,6 +597,7 @@ impl RuleProcessor for GmailClient { .await .map_err(Box::new)?; + eprintln!("[DEBUG] Batch trash completed: {} messages", count); Ok(()) } }