elektromail 0.1.0

A minimal, Rust-based IMAP + SMTP mail server for local development and testing
Documentation
use crate::store::FlagSet;

pub(super) fn parse_literal_size(line: &str) -> Option<usize> {
    let start = line.rfind('{')?;
    let end = line.rfind('}')?;
    if end <= start + 1 {
        return None;
    }
    line[start + 1..end].parse::<usize>().ok()
}

pub(super) fn parse_mailbox_name(token: &str) -> String {
    token.trim_matches('"').to_string()
}

pub(super) fn parse_append_mailbox(line: &str) -> String {
    let mut parts = line.split_whitespace();
    let _tag = parts.next();
    let cmd = parts.next().unwrap_or("");
    if !cmd.eq_ignore_ascii_case("APPEND") {
        return "INBOX".to_string();
    }
    let mailbox = parts.next().unwrap_or("INBOX");
    parse_mailbox_name(mailbox)
}

pub(super) fn parse_internal_date_from_line(line: &str) -> Option<String> {
    let mut in_quotes = false;
    let mut current = String::new();
    let mut candidate = None;
    for ch in line.chars() {
        if ch == '"' {
            if in_quotes {
                if looks_like_internal_date(&current) {
                    candidate = Some(current.clone());
                }
                current.clear();
                in_quotes = false;
            } else {
                in_quotes = true;
            }
        } else if in_quotes {
            current.push(ch);
        }
    }
    candidate
}

fn looks_like_internal_date(value: &str) -> bool {
    value.contains('-') && value.contains(':') && value.contains(' ')
}

pub(super) fn list_match(name: &str, pattern: &str) -> bool {
    if pattern == "*" || pattern == "%" {
        return true;
    }
    if pattern.is_empty() {
        return false;
    }
    if let Some(prefix) = pattern.strip_suffix('*') {
        return name.starts_with(prefix);
    }
    if let Some(prefix) = pattern.strip_suffix('%') {
        return name.starts_with(prefix);
    }
    name.eq_ignore_ascii_case(pattern)
}

pub(super) fn parse_imap_args(line: &str) -> Vec<String> {
    let mut args = Vec::new();
    let mut in_quotes = false;
    let mut current = String::new();
    let mut chars = line.chars().peekable();
    // Skip tag and command
    let mut skipped = 0;
    while let Some(ch) = chars.peek().copied() {
        if ch == ' ' {
            skipped += 1;
            chars.next();
            if skipped >= 2 {
                break;
            }
        } else {
            chars.next();
        }
    }

    for ch in chars {
        match ch {
            '"' => {
                in_quotes = !in_quotes;
            }
            ' ' if !in_quotes => {
                if !current.is_empty() {
                    args.push(current.clone());
                    current.clear();
                }
            }
            _ => current.push(ch),
        }
    }
    if !current.is_empty() {
        args.push(current);
    }
    args
}

pub(super) fn parse_sequence_set(token: &str, max: u32) -> Vec<u32> {
    let mut result = Vec::new();
    for part in token.split(',') {
        if let Some((start, end)) = part.split_once(':') {
            let start = start.parse::<u32>().unwrap_or(1);
            let end = if end == "*" {
                max.max(start)
            } else {
                end.parse::<u32>().unwrap_or(start)
            };
            for v in start..=end {
                result.push(v);
            }
        } else if part == "*" {
            result.push(max.max(1));
        } else if let Ok(v) = part.parse::<u32>() {
            result.push(v);
        }
    }
    result
}

pub(super) fn parse_flag_list_from_line(line: &str) -> FlagSet {
    let mut flags = FlagSet::default();
    let Some(start) = line.find('(') else {
        return flags;
    };
    let Some(end) = line[start..].find(')') else {
        return flags;
    };
    let content = &line[start + 1..start + end];
    for raw in content.split_whitespace() {
        match raw.to_ascii_uppercase().as_str() {
            "\\SEEN" => flags.seen = true,
            "\\FLAGGED" => flags.flagged = true,
            "\\DELETED" => flags.deleted = true,
            "\\ANSWERED" => flags.answered = true,
            "\\DRAFT" => flags.draft = true,
            _ => {}
        }
    }
    flags
}

pub(super) fn parse_copy_target(line: &str) -> Option<(String, String, bool)> {
    let mut parts = line.split_whitespace();
    let _tag = parts.next()?;
    let cmd = parts.next()?;
    let mut is_uid = false;
    let cmd = if cmd.eq_ignore_ascii_case("UID") {
        is_uid = true;
        parts.next()?
    } else {
        cmd
    };
    if !cmd.eq_ignore_ascii_case("COPY") {
        return None;
    }
    let seqset = parts.next()?.to_string();
    let dest = parts.next().unwrap_or("INBOX");
    Some((seqset, parse_mailbox_name(dest), is_uid))
}

pub(super) fn parse_move_target(line: &str) -> Option<(String, String, bool)> {
    let mut parts = line.split_whitespace();
    let _tag = parts.next()?;
    let cmd = parts.next()?;
    let mut is_uid = false;
    let cmd = if cmd.eq_ignore_ascii_case("UID") {
        is_uid = true;
        parts.next()?
    } else {
        cmd
    };
    if !cmd.eq_ignore_ascii_case("MOVE") {
        return None;
    }
    let seqset = parts.next()?.to_string();
    let dest = parts.next().unwrap_or("INBOX");
    Some((seqset, parse_mailbox_name(dest), is_uid))
}

pub(super) fn parse_uid_range(range: &str, max_uid: u32) -> (u32, u32) {
    if let Some((start, end)) = range.split_once(':') {
        let start = start.parse::<u32>().unwrap_or(1);
        let end = if end == "*" {
            max_uid.max(start)
        } else {
            end.parse::<u32>().unwrap_or(max_uid)
        };
        return (start, end);
    }
    let single = range.parse::<u32>().unwrap_or(1);
    (single, single)
}

pub(super) fn parse_seq_range(range: &str, max_seq: u32) -> (u32, u32) {
    if let Some((start, end)) = range.split_once(':') {
        let start = start.parse::<u32>().unwrap_or(1);
        let end = if end == "*" {
            max_seq.max(start)
        } else {
            end.parse::<u32>().unwrap_or(max_seq)
        };
        return (start, end);
    }
    let single = range.parse::<u32>().unwrap_or(1);
    (single, single)
}