mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
- Treat MIME parts with Content-ID but no Content-Disposition as inline - Add account_ids filter to CLI export search to avoid pulling all accounts - Skip failed emails during export instead of aborting the entire batch
216 lines
6.6 KiB
Rust
216 lines
6.6 KiB
Rust
use crate::api::download::download_and_export_with_json_header;
|
|
use crate::api::search::search_messages;
|
|
use crate::api::stats::fetch_account_stats;
|
|
use crate::BichonCliConfig;
|
|
use bichon_core::account::payload::MinimalAccount;
|
|
use console::style;
|
|
use dialoguer::Confirm;
|
|
use dialoguer::{theme::ColorfulTheme, Input};
|
|
use indicatif::{ProgressBar, ProgressStyle};
|
|
use reqwest::Client;
|
|
use std::path::{Path, PathBuf};
|
|
use sysinfo::Disks;
|
|
|
|
pub async fn handle_account_export(
|
|
config: &BichonCliConfig,
|
|
account: MinimalAccount,
|
|
theme: &ColorfulTheme,
|
|
) {
|
|
let client = Client::new();
|
|
|
|
println!("Fetching account statistics...");
|
|
let stats = match fetch_account_stats(&client, config, account.id).await {
|
|
Some(s) => s,
|
|
None => {
|
|
eprintln!("{} Failed to fetch account statistics.", style("✘").red());
|
|
return;
|
|
}
|
|
};
|
|
|
|
println!("\n--- Account Statistics ---");
|
|
println!(" Total Emails: {}", style(stats.total_count).cyan());
|
|
println!(
|
|
" Total Size: {}",
|
|
style(format_bytes(stats.total_size)).cyan()
|
|
);
|
|
|
|
let path = loop {
|
|
let input: String = Input::with_theme(theme)
|
|
.with_prompt("Enter ABSOLUTE directory path for MBOX file")
|
|
.interact_text()
|
|
.unwrap();
|
|
|
|
let p = PathBuf::from(&input);
|
|
|
|
if !p.is_absolute() {
|
|
eprintln!(
|
|
" {} {}",
|
|
style("✘").red(),
|
|
style("Invalid path: Must be an absolute path.").red()
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if !p.exists() {
|
|
eprintln!(
|
|
" {} {}",
|
|
style("✘").red(),
|
|
style("Invalid path: Directory does not exist.").red()
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if !p.is_dir() {
|
|
eprintln!(
|
|
" {} {}",
|
|
style("✘").red(),
|
|
style("Invalid path: The path provided is not a directory.").red()
|
|
);
|
|
continue;
|
|
}
|
|
break p;
|
|
};
|
|
|
|
let disks = Disks::new_with_refreshed_list();
|
|
let disk_result = disks
|
|
.list()
|
|
.iter()
|
|
.find(|d| path.starts_with(d.mount_point()))
|
|
.ok_or_else(|| "Could not identify the disk for the provided path.");
|
|
|
|
match disk_result {
|
|
Ok(disk) => {
|
|
let free_space = disk.available_space();
|
|
let required_space = (stats.total_size as f64 * 1.2) as u64;
|
|
|
|
if free_space < required_space {
|
|
eprintln!(
|
|
" {} Insufficient disk space (including 10% safety buffer)!\n Required: {} (Base: {})\n Available: {}",
|
|
style("✘").red(),
|
|
style(format_bytes(required_space)).yellow(),
|
|
style(format_bytes(stats.total_size)).yellow(),
|
|
style(format_bytes(free_space)).yellow()
|
|
);
|
|
return;
|
|
}
|
|
|
|
println!(
|
|
" {} Disk space check passed. (Required: {}, Available: {})",
|
|
style("✔").green(),
|
|
style(format_bytes(required_space)).cyan(),
|
|
style(format_bytes(free_space)).cyan()
|
|
);
|
|
}
|
|
Err(e) => {
|
|
eprintln!(" {} {}", style("✘").red(), style(e).red());
|
|
return;
|
|
}
|
|
}
|
|
let mbox_file = get_unique_mbox_path(&path, account.id, &account.email);
|
|
if Confirm::with_theme(theme)
|
|
.with_prompt(format!(
|
|
"Export {} emails to '{}'?",
|
|
stats.total_count,
|
|
mbox_file.display()
|
|
))
|
|
.default(true)
|
|
.interact()
|
|
.unwrap()
|
|
{
|
|
println!(
|
|
" {} Starting export ({} items per page)...",
|
|
style("✔").green(),
|
|
100
|
|
);
|
|
|
|
let pb = ProgressBar::new(stats.total_count as u64);
|
|
pb.set_style(ProgressStyle::with_template(
|
|
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}"
|
|
).unwrap());
|
|
|
|
let mut file = match tokio::fs::OpenOptions::new()
|
|
.append(true)
|
|
.create(true)
|
|
.open(&mbox_file)
|
|
.await
|
|
{
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
eprintln!(" ✘ Failed to open file '{}': {}", path.display(), e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let page_size = 100;
|
|
let mut current_page = 1;
|
|
let mut total_pages;
|
|
|
|
loop {
|
|
let account_ids = Some(std::collections::HashSet::from([account.id]));
|
|
if let Some(batch) = search_messages(&client, config, account_ids, current_page, page_size).await {
|
|
total_pages = batch.total_pages.unwrap();
|
|
|
|
pb.set_message(format!("Page {}/{}", current_page, total_pages));
|
|
|
|
for envelope in batch.items {
|
|
let success =
|
|
download_and_export_with_json_header(&client, config, envelope.clone(), &mut file)
|
|
.await;
|
|
|
|
if !success {
|
|
eprintln!(
|
|
" ✘ Failed to export email {}, skipping...",
|
|
envelope.id
|
|
);
|
|
continue;
|
|
}
|
|
pb.inc(1);
|
|
}
|
|
if current_page >= total_pages {
|
|
break;
|
|
}
|
|
current_page += 1;
|
|
} else {
|
|
pb.finish_with_message("Error");
|
|
eprintln!(
|
|
" ✘ Failed to fetch page {}. Aborting process...",
|
|
current_page
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
pb.finish();
|
|
println!(" {} Export complete!", style("✔").green());
|
|
}
|
|
}
|
|
|
|
fn format_bytes(bytes: u64) -> String {
|
|
if bytes < 1024 {
|
|
format!("{:.2} B", bytes)
|
|
} else if bytes < 1024 * 1024 {
|
|
format!("{:.2} KB", bytes / 1024)
|
|
} else if bytes < 1024 * 1024 * 1024 {
|
|
format!("{:.2} MB", bytes / 1024 / 1024)
|
|
} else {
|
|
format!("{:.2} GB", bytes / 1024 / 1024 / 1024)
|
|
}
|
|
}
|
|
|
|
fn get_unique_mbox_path(base_dir: &Path, account_id: u64, email: &str) -> PathBuf {
|
|
let email_part = email.replace(' ', "_");
|
|
|
|
let mut base_name = format!("account_{}_{}", account_id, email_part);
|
|
if base_name.starts_with('.') {
|
|
base_name = format!("_{}", base_name);
|
|
}
|
|
|
|
let mut final_path = base_dir.join(format!("{}.mbox", base_name));
|
|
|
|
let mut counter = 1;
|
|
while final_path.exists() {
|
|
final_path = base_dir.join(format!("{}_{}.mbox", base_name, counter));
|
|
counter += 1;
|
|
}
|
|
final_path
|
|
}
|