use std::cell::RefCell;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Local};
use uuid::Uuid;
use anyhow::{Context, Result};
use crate::config::Config;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: String,
pub from: String,
pub to: String,
pub body: String,
pub timestamp: DateTime<Local>,
pub read: bool,
pub starred: bool,
pub folder: Folder,
#[serde(default)]
pub uid: Option<u32>,
#[serde(default)]
pub message_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Folder {
Inbox,
Sent,
Starred,
Trash,
}
impl Folder {
pub fn label(&self) -> &'static str {
match self {
Folder::Inbox => "Inbox",
Folder::Sent => "Sent",
Folder::Starred => "Starred",
Folder::Trash => "Trash",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
pub id: String,
pub address: String,
pub user: String,
pub domain: String,
pub display_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct MailData {
messages: Vec<Message>,
contacts: HashMap<String, Contact>,
next_contact_id: u64,
}
pub struct MailStore {
data_path: PathBuf,
data: RefCell<MailData>,
}
impl MailStore {
pub fn new(cfg: &Config) -> Result<Self> {
let data_dir = dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("mail-rs");
fs::create_dir_all(&data_dir)?;
let data_path = data_dir.join("mailbox.json");
let data = if data_path.exists() {
let raw = fs::read_to_string(&data_path)
.with_context(|| format!("Cannot read {}", data_path.display()))?;
serde_json::from_str(&raw).unwrap_or_default()
} else {
MailData::default()
};
let store = Self { data_path, data: RefCell::new(data) };
store.ensure_contact(&cfg.identity)?;
store.migrate_mime_bodies()?;
Ok(store)
}
fn migrate_mime_bodies(&self) -> Result<()> {
let needs_fix: Vec<usize> = self
.data
.borrow()
.messages
.iter()
.enumerate()
.filter(|(_, m)| looks_like_raw_mime(&m.body))
.map(|(i, _)| i)
.collect();
if needs_fix.is_empty() {
return Ok(());
}
for i in needs_fix {
let raw_body = self.data.borrow().messages[i].body.clone();
let cleaned = crate::transport::extract_body_text_pub(raw_body.as_bytes());
if !cleaned.is_empty() && cleaned != raw_body {
self.data.borrow_mut().messages[i].body = cleaned;
}
}
self.save()?;
Ok(())
}
fn save(&self) -> Result<()> {
let json = serde_json::to_string_pretty(&*self.data.borrow())?;
fs::write(&self.data_path, json)?;
Ok(())
}
pub fn ensure_contact(&self, address: &str) -> Result<Contact> {
{
let data = self.data.borrow();
if let Some(c) = data.contacts.get(address) {
return Ok(c.clone());
}
}
let (user, domain) = split_address(address);
let mut data = self.data.borrow_mut();
data.next_contact_id += 1;
let contact = Contact {
id: data.next_contact_id.to_string(),
address: address.to_string(),
user,
domain,
display_name: None,
};
data.contacts.insert(address.to_string(), contact.clone());
drop(data);
self.save()?;
Ok(contact)
}
pub fn ensure_contact_with_name(&self, address: &str, name: Option<&str>) -> Result<Contact> {
let mut contact = self.ensure_contact(address)?;
if let Some(n) = name {
if contact.display_name.as_deref() != Some(n) {
contact.display_name = Some(n.to_string());
self.data
.borrow_mut()
.contacts
.insert(address.to_string(), contact.clone());
self.save()?;
}
}
Ok(contact)
}
pub fn resolve_contact_id(&self, id: &str) -> Result<String> {
self.data
.borrow()
.contacts
.values()
.find(|c| c.id == id)
.map(|c| c.address.clone())
.with_context(|| format!("No contact with ID {}", id))
}
pub fn contacts_by_domain(&self) -> HashMap<String, Vec<Contact>> {
let mut map: HashMap<String, Vec<Contact>> = HashMap::new();
for c in self.data.borrow().contacts.values() {
map.entry(c.domain.clone()).or_default().push(c.clone());
}
for list in map.values_mut() {
list.sort_by(|a, b| {
a.id.parse::<u64>()
.unwrap_or(0)
.cmp(&b.id.parse::<u64>().unwrap_or(0))
});
}
map
}
pub fn all_contacts(&self) -> Vec<Contact> {
let mut v: Vec<Contact> = self.data.borrow().contacts.values().cloned().collect();
v.sort_by(|a, b| {
a.id.parse::<u64>()
.unwrap_or(0)
.cmp(&b.id.parse::<u64>().unwrap_or(0))
});
v
}
pub fn record_sent(&self, to: &str, body: &str) -> Result<()> {
self.ensure_contact(to)?;
let msg = Message {
id: Uuid::new_v4().to_string(),
from: String::new(),
to: to.to_string(),
body: body.to_string(),
timestamp: Local::now(),
read: true,
starred: false,
folder: Folder::Sent,
uid: None,
message_id: None,
};
self.data.borrow_mut().messages.push(msg);
self.save()
}
pub fn deliver(
&self,
from: &str,
body: &str,
display_name: Option<&str>,
uid: Option<u32>,
message_id: Option<String>,
) -> Result<bool> {
if let Some(ref mid) = message_id {
if self.has_message_id(mid) {
return Ok(false);
}
}
self.ensure_contact_with_name(from, display_name)?;
let msg = Message {
id: Uuid::new_v4().to_string(),
from: from.to_string(),
to: String::new(),
body: body.to_string(),
timestamp: Local::now(),
read: false,
starred: false,
folder: Folder::Inbox,
uid,
message_id,
};
self.data.borrow_mut().messages.push(msg);
self.save()?;
Ok(true)
}
pub fn has_message_id(&self, mid: &str) -> bool {
self.data
.borrow()
.messages
.iter()
.any(|m| m.message_id.as_deref() == Some(mid))
}
pub fn messages_in(&self, folder: &Folder) -> Vec<(usize, Message)> {
self.data
.borrow()
.messages
.iter()
.enumerate()
.filter(|(_, m)| {
if folder == &Folder::Starred {
m.starred
} else {
&m.folder == folder
}
})
.map(|(i, m)| (i, m.clone()))
.collect()
}
pub fn get_message(&self, index: usize) -> Option<Message> {
self.data.borrow().messages.get(index).cloned()
}
pub fn mark_read(&self, index: usize) -> Result<()> {
if let Some(m) = self.data.borrow_mut().messages.get_mut(index) {
m.read = true;
}
self.save()
}
pub fn toggle_star(&self, index: usize) -> Result<()> {
if let Some(m) = self.data.borrow_mut().messages.get_mut(index) {
m.starred = !m.starred;
}
self.save()
}
pub fn move_to_trash(&self, index: usize) -> Result<()> {
if let Some(m) = self.data.borrow_mut().messages.get_mut(index) {
m.folder = Folder::Trash;
}
self.save()
}
pub fn unread_count(&self) -> usize {
self.data
.borrow()
.messages
.iter()
.filter(|m| !m.read && m.folder == Folder::Inbox)
.count()
}
pub fn unread_in(&self, folder: &Folder) -> usize {
self.data
.borrow()
.messages
.iter()
.filter(|m| !m.read && &m.folder == folder)
.count()
}
}
fn looks_like_raw_mime(body: &str) -> bool {
body.contains("Content-Type:") || body.contains("Content-Transfer-Encoding:")
}
fn split_address(addr: &str) -> (String, String) {
if let Some(at) = addr.rfind('@') {
(addr[..at].to_string(), addr[at + 1..].to_string())
} else {
(addr.to_string(), "unknown".to_string())
}
}