fix: path issues on linux
This commit is contained in:
121
CLAUDE.md
Normal file
121
CLAUDE.md
Normal file
@@ -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<google_gmail1::Error>)`: 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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user