slashmail 0.1.0

CLI for searching, managing, and bulk-operating on emails via IMAP
use anyhow::{Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;

use crate::connection::ImapSession;
use crate::display::display_messages;
use crate::search::{self, SearchCriteria};

fn spinner(msg: &str) -> ProgressBar {
    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.cyan} {msg}")
            .unwrap(),
    );
    pb.set_message(msg.to_string());
    pb.enable_steady_tick(Duration::from_millis(80));
    pb
}

pub fn search_and_move(
    session: &mut ImapSession,
    criteria: &SearchCriteria,
    dest: &str,
    yes: bool,
    dry_run: bool,
) -> Result<()> {
    let sp = spinner("Searching...");
    let messages = search::search(session, criteria)?;
    sp.finish_and_clear();

    if messages.is_empty() {
        println!("No messages match the criteria.");
        return Ok(());
    }

    display_messages(&messages);

    if dry_run {
        println!(
            "Dry run: {} message(s) would be moved to {dest}.",
            messages.len()
        );
        return Ok(());
    }

    if !yes {
        let confirm =
            inquire::Confirm::new(&format!("Move {} message(s) to {dest}?", messages.len()))
                .with_default(false)
                .prompt()
                .context("Prompt failed")?;

        if !confirm {
            println!("Aborted.");
            return Ok(());
        }
    }

    let sp = spinner(&format!("Moving to {dest}..."));

    // Group by folder for multi-folder moves
    let mut by_folder: std::collections::HashMap<String, Vec<u32>> =
        std::collections::HashMap::new();
    for msg in &messages {
        let folder = msg
            .folder
            .clone()
            .unwrap_or_else(|| criteria.folder.clone());
        by_folder.entry(folder).or_default().push(msg.uid);
    }

    let mut total = 0usize;
    for (folder, uids) in &by_folder {
        session
            .select(folder)
            .with_context(|| format!("Failed to select '{folder}'"))?;

        for chunk in &search::build_uid_set(uids) {
            session
                .uid_move_or_fallback(chunk, dest)
                .with_context(|| format!("Failed to move messages from '{folder}' to {dest}"))?;
        }

        total += uids.len();
    }

    sp.finish_and_clear();
    println!("Moved {total} message(s) to {dest}.");
    Ok(())
}

pub fn delete(
    session: &mut ImapSession,
    criteria: &SearchCriteria,
    yes: bool,
    dry_run: bool,
) -> Result<()> {
    search_and_move(session, criteria, "Trash", yes, dry_run)
}