use crate::config::Config;
use crate::messages::{Message, MessageDb, MessageSync};
use anyhow::Result;
use std::path::PathBuf;
const MESSAGE_ENDPOINT: &str = "/message";
pub fn get_message_db_path(config: &Config) -> Result<PathBuf> {
let biovault_dir = config.get_biovault_dir()?;
let db_path = biovault_dir.join("data").join("messages.db");
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(db_path)
}
pub fn init_message_system(config: &Config) -> Result<(MessageDb, MessageSync)> {
let db_path = get_message_db_path(config)?;
let db = MessageDb::new(&db_path)?;
let data_dir = config.get_syftbox_data_dir()?;
let app = crate::syftbox::SyftBoxApp::new(&data_dir, &config.email, "biovault")?;
app.register_endpoint(MESSAGE_ENDPOINT)?;
let sync = MessageSync::new(&db_path, app)?;
println!("BioVault messaging initialized for {}", config.email);
Ok((db, sync))
}
pub fn send_message(
config: &Config,
recipient: &str,
body: &str,
subject: Option<&str>,
) -> Result<()> {
let (db, sync) = init_message_system(config)?;
let _ = sync.sync_quiet();
let mut msg = Message::new(
config.email.clone(),
recipient.to_string(),
body.to_string(),
);
if let Some(subj) = subject {
msg.subject = Some(subj.to_string());
}
db.insert_message(&msg)?;
sync.send_message(&msg.id)?;
println!("✉️ Message sent to {}", recipient);
if let Some(subj) = &msg.subject {
println!(" Subject: {}", subj);
}
Ok(())
}
pub fn reply_message(config: &Config, message_id: &str, body: &str) -> Result<()> {
let (db, sync) = init_message_system(config)?;
let _ = sync.sync_quiet();
let original = db
.get_message(message_id)?
.ok_or_else(|| anyhow::anyhow!("Message not found: {}", message_id))?;
let reply = Message::reply_to(&original, config.email.clone(), body.to_string());
db.insert_message(&reply)?;
sync.send_message(&reply.id)?;
println!("↩️ Reply sent to {}", reply.to);
Ok(())
}
pub fn delete_message(config: &Config, message_id: &str) -> Result<()> {
let (db, _) = init_message_system(config)?;
let msg = db
.get_message(message_id)?
.ok_or_else(|| anyhow::anyhow!("Message not found: {}", message_id))?;
db.delete_message(&msg.id)?;
println!(
"🗑️ Message deleted: {} ({})",
&msg.id[..8],
msg.display_subject()
);
Ok(())
}
pub fn list_messages(config: &Config, unread_only: bool) -> Result<()> {
let (db, sync) = init_message_system(config)?;
let (_new_msg_ids, count) = sync.sync_quiet()?;
if count > 0 {
println!("🆕 {} new message(s) received", count);
}
let messages = if unread_only {
db.list_unread_messages()?
} else {
db.list_messages(Some(50))?
};
if messages.is_empty() {
if unread_only {
println!("No unread messages");
} else {
println!("No messages");
}
return Ok(());
}
println!("\n📬 Messages:");
println!("─────────────");
for msg in messages {
let status_icon = match msg.status {
crate::messages::MessageStatus::Draft => "📝",
crate::messages::MessageStatus::Sent => "📤",
crate::messages::MessageStatus::Received => "📥",
crate::messages::MessageStatus::Read => "👁️",
crate::messages::MessageStatus::Deleted => "🗑️",
crate::messages::MessageStatus::Archived => "📁",
};
println!("\n{} [{}]", status_icon, &msg.id[..8]);
println!(" From: {}", msg.from);
println!(" To: {}", msg.to);
println!(" Subject: {}", msg.display_subject());
let local_time = msg.created_at.with_timezone(&chrono::Local);
println!(" Date: {}", local_time.format("%Y-%m-%d %H:%M:%S %Z"));
let preview = if msg.body.len() > 100 {
format!("{}...", &msg.body[..100])
} else {
msg.body.clone()
};
println!(" Body: {}", preview);
if msg.parent_id.is_some() {
println!(" ↩️ Reply to: {}", msg.parent_id.as_ref().unwrap());
}
}
Ok(())
}
pub fn read_message(config: &Config, message_id: &str) -> Result<()> {
let (db, sync) = init_message_system(config)?;
let _ = sync.sync_quiet();
let msg = db
.get_message(message_id)?
.ok_or_else(|| anyhow::anyhow!("Message not found: {}", message_id))?;
if msg.status == crate::messages::MessageStatus::Received {
db.mark_as_read(message_id)?;
}
println!("\n📧 Message Details");
println!("═══════════════════");
println!("ID: {}", msg.id);
println!("From: {}", msg.from);
println!("To: {}", msg.to);
println!("Subject: {}", msg.display_subject());
let local_time = msg.created_at.with_timezone(&chrono::Local);
println!("Date: {}", local_time.format("%Y-%m-%d %H:%M:%S %Z"));
if let Some(parent_id) = &msg.parent_id {
println!("Reply to: {}", parent_id);
}
if let Some(thread_id) = &msg.thread_id {
println!("Thread: {}", thread_id);
}
println!("\nBody:");
println!("─────");
println!("{}", msg.body);
Ok(())
}
pub fn view_thread(config: &Config, thread_id: &str) -> Result<()> {
let (db, sync) = init_message_system(config)?;
let _ = sync.sync_quiet();
let messages = db.get_thread_messages(thread_id)?;
if messages.is_empty() {
println!("No messages found in thread: {}", thread_id);
return Ok(());
}
println!("\n💬 Thread: {}", thread_id);
println!("═══════════════════════════");
for msg in messages {
let local_time = msg.created_at.with_timezone(&chrono::Local);
println!("\n[{}] {}", local_time.format("%Y-%m-%d %H:%M"), msg.from);
if let Some(subj) = &msg.subject {
println!("Subject: {}", subj);
}
println!("{}", msg.body);
println!("─────────────────────");
}
Ok(())
}
pub fn sync_messages(config: &Config) -> Result<()> {
let (_, sync) = init_message_system(config)?;
println!("🔄 Syncing messages...");
sync.sync()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_config() -> Config {
Config {
email: "test@example.com".to_string(),
syftbox_config: None,
}
}
#[test]
fn test_init_message_system() -> Result<()> {
let temp_dir = TempDir::new()?;
crate::config::set_test_syftbox_data_dir(temp_dir.path());
crate::config::set_test_biovault_home(temp_dir.path().join(".biovault_test"));
let config = create_test_config();
let db_path = get_message_db_path(&config)?;
let db = MessageDb::new(&db_path)?;
let messages = db.list_messages(None)?;
assert_eq!(messages.len(), 0);
Ok(())
}
#[test]
fn test_message_crud() -> Result<()> {
let temp_dir = TempDir::new()?;
crate::config::set_test_syftbox_data_dir(temp_dir.path());
crate::config::set_test_biovault_home(temp_dir.path().join(".biovault_test"));
let config = create_test_config();
let db_path = get_message_db_path(&config)?;
let db = MessageDb::new(&db_path)?;
let msg = Message::new(
"test@example.com".to_string(),
"recipient@example.com".to_string(),
"Test message body".to_string(),
);
db.insert_message(&msg)?;
let retrieved = db.get_message(&msg.id)?;
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().body, "Test message body");
let messages = db.list_messages(None)?;
assert_eq!(messages.len(), 1);
db.delete_message(&msg.id)?;
let deleted_msg = db.get_message(&msg.id)?;
assert!(deleted_msg.is_some());
assert_eq!(
deleted_msg.unwrap().status,
crate::messages::MessageStatus::Deleted
);
let messages_after_delete = db.list_messages(None)?;
assert_eq!(messages_after_delete.len(), 0);
Ok(())
}
}