pop3-codec 0.3.0

Low-level POP3 Parser and Serializer
Documentation
use std::str::from_utf8;

use nom::{
    branch::alt,
    bytes::streaming::{tag, tag_no_case, take_while, take_while1},
    character::streaming::not_line_ending,
    combinator::{map, map_res, opt, recognize, value},
    sequence::{preceded, tuple},
    IResult,
};

use crate::{
    parse::{language, number},
    types::command::{Command, Language},
};

pub(crate) fn user(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = tuple((
        tag_no_case("USER"),
        tag(" "),
        map_res(not_line_ending, from_utf8),
    ));

    let (remaining, (_, _, name)) = parser(input)?;

    Ok((remaining, Command::User(name.into())))
}

pub(crate) fn pass(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = tuple((
        tag_no_case("PASS"),
        tag(" "),
        map_res(not_line_ending, from_utf8),
    ));

    let (remaining, (_, _, pass)) = parser(input)?;

    Ok((remaining, Command::Pass(pass.into())))
}

pub(crate) fn stat(input: &[u8]) -> IResult<&[u8], Command> {
    value(Command::Stat, tag_no_case("STAT"))(input)
}

pub(crate) fn list(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = tuple((
        tag_no_case("LIST"),
        opt(map(tuple((tag(" "), number)), |(_, msg)| msg)),
    ));

    let (remaining, (_, maybe_msg)) = parser(input)?;

    Ok((
        remaining,
        match maybe_msg {
            Some(msg) => Command::List { msg },
            None => Command::ListAll,
        },
    ))
}

pub(crate) fn retr(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = tuple((tag_no_case("RETR"), tag(" "), number));

    let (remaining, (_, _, msg)) = parser(input)?;

    Ok((remaining, Command::Retr { msg }))
}

pub(crate) fn dele(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = tuple((tag_no_case("DELE"), tag(" "), number));

    let (remaining, (_, _, msg)) = parser(input)?;

    Ok((remaining, Command::Dele { msg }))
}

pub(crate) fn noop(input: &[u8]) -> IResult<&[u8], Command> {
    value(Command::Noop, tag_no_case("NOOP"))(input)
}

pub(crate) fn rset(input: &[u8]) -> IResult<&[u8], Command> {
    value(Command::Rset, tag_no_case("RSET"))(input)
}

pub(crate) fn quit(input: &[u8]) -> IResult<&[u8], Command> {
    value(Command::Quit, tag_no_case("QUIT"))(input)
}

pub(crate) fn apop(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = tuple((
        tag_no_case("APOP"),
        tag(" "),
        map_res(take_while(|byte| byte != b' '), from_utf8),
        tag(" "),
        map_res(not_line_ending, from_utf8),
    ));

    let (remaining, (_, _, name, _, digest)) = parser(input)?;

    Ok((
        remaining,
        Command::Apop {
            name: name.into(),
            digest: digest.into(),
        },
    ))
}

pub(crate) fn top(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = tuple((tag_no_case("TOP"), tag(" "), number, tag(" "), number));

    let (remaining, (_, _, msg, _, n)) = parser(input)?;

    Ok((remaining, Command::Top { msg, n }))
}

pub(crate) fn uidl(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = preceded(tag_no_case("UIDL"), opt(preceded(tag(" "), number)));

    let (remaining, maybe_msg) = parser(input)?;

    Ok((
        remaining,
        match maybe_msg {
            Some(msg) => Command::Uidl { msg },
            None => Command::UidlAll,
        },
    ))
}

pub(crate) fn capa(input: &[u8]) -> IResult<&[u8], Command> {
    value(Command::Capa, tag_no_case("CAPA"))(input)
}

pub(crate) fn stls(input: &[u8]) -> IResult<&[u8], Command> {
    value(Command::Stls, tag_no_case("STLS"))(input)
}

pub(crate) fn auth(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = alt((
        map(
            tuple((
                tag_no_case(b"AUTH"),
                tag(" "),
                auth_type,
                opt(map(
                    tuple((
                        tag(" "),
                        alt((base64, map_res(tag("="), std::str::from_utf8))),
                    )),
                    |(_, maybe_ir)| maybe_ir,
                )),
            )),
            |(_, _, mechanism, initial_response)| Command::Auth {
                mechanism: mechanism.to_owned(),
                initial_response: initial_response.map(|i| i.to_owned()),
            },
        ),
        map(tag_no_case("AUTH"), |_| Command::AuthAll),
    ));

    let (remaining, cmd) = parser(input)?;

    Ok((remaining, cmd))
}

pub(crate) fn utf8(input: &[u8]) -> IResult<&[u8], Command> {
    value(Command::Utf8, tag_no_case("UTF8"))(input)
}

pub(crate) fn lang(input: &[u8]) -> IResult<&[u8], Command> {
    let mut parser = preceded(tag_no_case("LANG"), opt(preceded(tag(" "), lang_or_wild)));

    let (remaining, maybe_lang) = parser(input)?;

    Ok((
        remaining,
        match maybe_lang {
            Some(lang_or_wild) => Command::Lang { lang_or_wild },
            None => Command::LangAll,
        },
    ))
}

// -------------------------------------------------------------------------------------------------

fn auth_type(input: &[u8]) -> IResult<&[u8], &str> {
    map_res(take_while1(is_auth_char), std::str::from_utf8)(input)
}

fn is_auth_char(i: u8) -> bool {
    is_alpha(i) || is_digit(i) || i == b'-' || i == b'_'
}

fn base64(input: &[u8]) -> IResult<&[u8], &str> {
    let mut parser = map_res(
        recognize(tuple((
            take_while(is_base64_char),
            opt(alt((tag("=="), tag("=")))),
        ))),
        from_utf8,
    );

    let (remaining, base64) = parser(input)?;

    Ok((remaining, base64))
}

fn is_base64_char(byte: u8) -> bool {
    is_alpha(byte) || is_digit(byte) || byte == b'+' || byte == b'/'
}

fn is_alpha(byte: u8) -> bool {
    matches!(byte, b'a'..=b'z' | b'A'..=b'Z')
}

fn is_digit(byte: u8) -> bool {
    matches!(byte, b'0'..=b'9')
}

fn lang_or_wild(input: &[u8]) -> IResult<&[u8], Language> {
    alt((
        value(Language::Wild, tag("*")),
        map(language, |lang| Language::Lang(lang.to_string())),
    ))(input)
}