use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::store::{FlagSet, Store, current_internal_date};
#[derive(Deserialize)]
struct PreloadMeta {
flags: Option<Vec<String>>,
internal_date: Option<String>,
}
pub(crate) fn load_from_env(store: &mut Store) -> io::Result<Option<PathBuf>> {
let Ok(dir) = std::env::var("ELEKTROMAIL_PRELOAD_DIR") else {
return Ok(None);
};
let trimmed = dir.trim();
if trimmed.is_empty() {
return Ok(None);
}
let path = PathBuf::from(trimmed);
load_from_dir(store, &path)?;
Ok(Some(path))
}
pub(crate) fn load_from_dir(store: &mut Store, path: &Path) -> io::Result<()> {
let mut user_dirs = list_dirs(path)?;
user_dirs.sort_by_key(|entry| entry.file_name());
for user_dir in user_dirs {
let user = user_dir.file_name().to_string_lossy().to_string();
if user.starts_with('.') {
continue;
}
let mut mailbox_dirs = list_dirs(&user_dir.path())?;
mailbox_dirs.sort_by_key(|entry| entry.file_name());
for mailbox_dir in mailbox_dirs {
let mailbox = mailbox_dir.file_name().to_string_lossy().to_string();
if mailbox.starts_with('.') {
continue;
}
store.ensure_mailbox(&user, &mailbox);
let mut messages = list_files(&mailbox_dir.path(), "eml")?;
messages.sort_by_key(|entry| entry.file_name());
for message in messages {
let message_path = message.path();
let data = fs::read(&message_path)?;
let meta = load_meta(&message_path)?;
let internal_date = meta
.as_ref()
.and_then(|meta| meta.internal_date.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(current_internal_date);
let flags = meta
.as_ref()
.and_then(|meta| meta.flags.as_ref())
.map(parse_flags)
.unwrap_or_default();
store.append_with_flags(&user, &mailbox, data, internal_date, &flags);
}
}
}
Ok(())
}
fn list_dirs(path: &Path) -> io::Result<Vec<fs::DirEntry>> {
let mut entries = Vec::new();
if !path.exists() {
return Ok(entries);
}
for entry in fs::read_dir(path)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
entries.push(entry);
}
}
Ok(entries)
}
fn list_files(path: &Path, extension: &str) -> io::Result<Vec<fs::DirEntry>> {
let mut entries = Vec::new();
if !path.exists() {
return Ok(entries);
}
for entry in fs::read_dir(path)? {
let entry = entry?;
if entry.file_type()?.is_file() {
let matches = entry
.path()
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case(extension));
if matches {
entries.push(entry);
}
}
}
Ok(entries)
}
fn load_meta(message_path: &Path) -> io::Result<Option<PreloadMeta>> {
let meta_path = message_path.with_extension("eml.meta.json");
if !meta_path.exists() {
return Ok(None);
}
let data = fs::read_to_string(meta_path)?;
let meta: PreloadMeta = serde_json::from_str(&data)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
Ok(Some(meta))
}
fn parse_flags(flags: &Vec<String>) -> FlagSet {
let mut set = FlagSet::default();
for flag in flags {
let normalized = flag.trim().trim_start_matches('\\').to_ascii_uppercase();
match normalized.as_str() {
"SEEN" => set.seen = true,
"FLAGGED" => set.flagged = true,
"DELETED" => set.deleted = true,
"ANSWERED" => set.answered = true,
"DRAFT" => set.draft = true,
_ => {}
}
}
set
}