use crate::output::{OutputFormat, print_output};
use anyhow::Result;
use clap::{Args, Subcommand};
use grr_gmail::GmailClient;
#[derive(Subcommand, Debug)]
pub enum MessageOpsCommands {
Label(LabelArgs),
Trash(TrashArgs),
Untrash(UntrashArgs),
Delete(DeleteArgs),
BatchLabel(BatchLabelArgs),
BatchDelete(BatchDeleteArgs),
}
#[derive(Args, Debug)]
pub struct LabelArgs {
pub message_id: String,
#[arg(long, value_delimiter = ',')]
pub add: Vec<String>,
#[arg(long, value_delimiter = ',')]
pub remove: Vec<String>,
#[arg(short, long, value_enum, default_value = "json")]
pub format: OutputFormat,
}
#[derive(Args, Debug)]
pub struct TrashArgs {
pub message_id: String,
#[arg(short, long, value_enum, default_value = "json")]
pub format: OutputFormat,
}
#[derive(Args, Debug)]
pub struct UntrashArgs {
pub message_id: String,
#[arg(short, long, value_enum, default_value = "json")]
pub format: OutputFormat,
}
#[derive(Args, Debug)]
pub struct DeleteArgs {
pub message_id: String,
}
#[derive(Args, Debug)]
pub struct BatchLabelArgs {
#[arg(long, value_delimiter = ',', conflicts_with = "search")]
pub ids: Vec<String>,
#[arg(long, conflicts_with = "ids")]
pub search: Option<String>,
#[arg(short, long, default_value = "10000", requires = "search")]
pub max: usize,
#[arg(long)]
pub dry_run: bool,
#[arg(long, value_delimiter = ',')]
pub add: Vec<String>,
#[arg(long, value_delimiter = ',')]
pub remove: Vec<String>,
}
#[derive(Args, Debug)]
pub struct BatchDeleteArgs {
#[arg(long, value_delimiter = ',', conflicts_with = "search")]
pub ids: Vec<String>,
#[arg(long, conflicts_with = "ids")]
pub search: Option<String>,
#[arg(short, long, default_value = "10000", requires = "search")]
pub max: usize,
#[arg(long)]
pub dry_run: bool,
}
const BATCH_API_CHUNK: usize = 1000;
async fn resolve_ids(
client: &GmailClient,
ids: Vec<String>,
search: Option<&str>,
max: usize,
) -> Result<Vec<String>> {
if let Some(query) = search {
let matched = client.search(query, max).await?;
if matched.len() >= max {
eprintln!("warning: matched at least {max} messages; raise --max or narrow the query");
}
Ok(matched.into_iter().map(|m| m.id).collect())
} else {
Ok(ids)
}
}
pub async fn handle_message_ops_cmd(client: &GmailClient, cmd: MessageOpsCommands) -> Result<()> {
match cmd {
MessageOpsCommands::Label(args) => {
let msg = client
.modify_labels(&args.message_id, &args.add, &args.remove)
.await?;
print_output(&msg, args.format)?;
}
MessageOpsCommands::Trash(args) => {
let msg = client.trash_message(&args.message_id).await?;
print_output(&msg, args.format)?;
}
MessageOpsCommands::Untrash(args) => {
let msg = client.untrash_message(&args.message_id).await?;
print_output(&msg, args.format)?;
}
MessageOpsCommands::Delete(args) => {
client.delete_message(&args.message_id).await?;
println!("Message {} permanently deleted", args.message_id);
}
MessageOpsCommands::BatchLabel(args) => {
let ids = resolve_ids(client, args.ids, args.search.as_deref(), args.max).await?;
if args.dry_run {
println!(
"dry-run: would update labels ({:+} / {:-}) on {} messages",
args.add.len(),
args.remove.len(),
ids.len()
);
return Ok(());
}
for chunk in ids.chunks(BATCH_API_CHUNK) {
client
.batch_modify_labels(chunk, &args.add, &args.remove)
.await?;
}
println!("Labels updated for {} messages", ids.len());
}
MessageOpsCommands::BatchDelete(args) => {
let ids = resolve_ids(client, args.ids, args.search.as_deref(), args.max).await?;
if args.dry_run {
println!("dry-run: would permanently delete {} messages", ids.len());
return Ok(());
}
for chunk in ids.chunks(BATCH_API_CHUNK) {
client.batch_delete_messages(chunk).await?;
}
println!("{} messages permanently deleted", ids.len());
}
}
Ok(())
}