elektromail 0.1.1

A minimal, Rust-based IMAP + SMTP mail server for local development and testing
Documentation
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
}