sim-lib-server 0.1.0

SIM workspace package for sim lib server.
Documentation
#[cfg(any(feature = "trigger-imap", feature = "trigger-smtp"))]
use std::io::{BufRead, BufReader, Read, Write};
#[cfg(any(
    feature = "trigger-imap",
    feature = "trigger-smtp",
    feature = "trigger-telegram",
    feature = "trigger-matrix"
))]
use std::net::TcpStream;
#[cfg(any(
    feature = "trigger-imap",
    feature = "trigger-smtp",
    feature = "trigger-telegram",
    feature = "trigger-matrix"
))]
use std::time::Duration;

#[cfg(any(
    feature = "trigger-webhook",
    feature = "trigger-imap",
    feature = "trigger-smtp",
    feature = "trigger-telegram",
    feature = "trigger-matrix"
))]
use sim_kernel::{Error, Result};

#[cfg(any(feature = "trigger-imap", feature = "trigger-smtp"))]
pub(super) fn parse_host_port(address: &str, default_port: u16) -> Result<(String, u16)> {
    match address.rsplit_once(':') {
        Some((host, port)) => Ok((
            host.to_owned(),
            port.parse::<u16>()
                .map_err(|_| Error::Eval(format!("invalid port in address {address}")))?,
        )),
        None => Ok((address.to_owned(), default_port)),
    }
}

#[cfg(any(
    feature = "trigger-imap",
    feature = "trigger-smtp",
    feature = "trigger-telegram",
    feature = "trigger-matrix"
))]
pub(super) fn connect_with_timeout(host: &str, port: u16, timeout: Duration) -> Result<TcpStream> {
    let addr = format!("{host}:{port}");
    let stream = TcpStream::connect(addr).map_err(io_to_host)?;
    stream.set_read_timeout(Some(timeout)).map_err(io_to_host)?;
    stream
        .set_write_timeout(Some(timeout))
        .map_err(io_to_host)?;
    Ok(stream)
}

#[cfg(any(
    feature = "trigger-webhook",
    feature = "trigger-imap",
    feature = "trigger-smtp",
    feature = "trigger-telegram",
    feature = "trigger-matrix"
))]
pub(super) fn io_to_host(error: std::io::Error) -> Error {
    Error::HostError(error.to_string())
}

#[cfg(feature = "trigger-smtp")]
pub(super) fn smtp_command(
    reader: &mut BufReader<TcpStream>,
    command: &str,
    expected: u16,
) -> Result<()> {
    reader
        .get_mut()
        .write_all(command.as_bytes())
        .map_err(io_to_host)?;
    reader.get_mut().flush().map_err(io_to_host)?;
    expect_smtp_code(reader, expected)
}

#[cfg(feature = "trigger-smtp")]
pub(super) fn expect_smtp_code(reader: &mut BufReader<TcpStream>, expected: u16) -> Result<()> {
    loop {
        let line = read_line(reader)?;
        let Some(prefix) = line.get(0..3) else {
            return Err(Error::HostError("short smtp response".to_owned()));
        };
        let code = prefix
            .parse::<u16>()
            .map_err(|_| Error::HostError("invalid smtp status code".to_owned()))?;
        if code != expected {
            return Err(Error::Eval(format!(
                "smtp expected {expected}, received {code}"
            )));
        }
        if line.as_bytes().get(3) != Some(&b'-') {
            return Ok(());
        }
    }
}

#[cfg(feature = "trigger-imap")]
pub(super) fn imap_command_ok(
    reader: &mut BufReader<TcpStream>,
    tag_counter: &mut u32,
    command: &str,
) -> Result<()> {
    let _ = imap_command_collect(reader, tag_counter, command)?;
    Ok(())
}

#[cfg(feature = "trigger-imap")]
pub(super) fn imap_command_collect(
    reader: &mut BufReader<TcpStream>,
    tag_counter: &mut u32,
    command: &str,
) -> Result<Vec<String>> {
    let tag = format!("A{tag_counter:04}");
    *tag_counter += 1;
    reader
        .get_mut()
        .write_all(format!("{tag} {command}\r\n").as_bytes())
        .map_err(io_to_host)?;
    reader.get_mut().flush().map_err(io_to_host)?;
    let mut lines = Vec::new();
    loop {
        let line = read_imap_line(reader)?;
        if line.starts_with(&tag) {
            if !line.contains("OK") {
                return Err(Error::Eval(format!("imap command failed: {line}")));
            }
            return Ok(lines);
        }
        lines.push(line);
    }
}

#[cfg(feature = "trigger-imap")]
pub(super) fn first_search_result(lines: &[String]) -> Option<u32> {
    lines.iter().find_map(|line| {
        let suffix = line.strip_prefix("* SEARCH ")?;
        suffix.split_whitespace().next()?.parse::<u32>().ok()
    })
}

#[cfg(feature = "trigger-imap")]
pub(super) fn read_imap_fetch_body(
    reader: &mut BufReader<TcpStream>,
    tag: &str,
) -> Result<Vec<u8>> {
    let mut body = Vec::new();
    loop {
        let line = read_imap_line(reader)?;
        if line.starts_with(tag) {
            if !line.contains("OK") {
                return Err(Error::Eval(format!("imap fetch failed: {line}")));
            }
            return Ok(body);
        }
        if let Some(len) = imap_literal_len(&line)? {
            body.resize(len, 0);
            reader.read_exact(&mut body).map_err(io_to_host)?;
        }
    }
}

#[cfg(feature = "trigger-imap")]
fn imap_literal_len(line: &str) -> Result<Option<usize>> {
    let Some(start) = line.rfind('{') else {
        return Ok(None);
    };
    let Some(end) = line[start..].find('}') else {
        return Ok(None);
    };
    let digits = &line[start + 1..start + end];
    let len = digits
        .parse::<usize>()
        .map_err(|_| Error::HostError("invalid imap literal length".to_owned()))?;
    Ok(Some(len))
}

#[cfg(feature = "trigger-imap")]
pub(super) fn read_imap_line(reader: &mut BufReader<TcpStream>) -> Result<String> {
    read_line(reader)
}

#[cfg(any(feature = "trigger-imap", feature = "trigger-smtp"))]
pub(super) fn read_line(reader: &mut BufReader<TcpStream>) -> Result<String> {
    let mut line = String::new();
    let read = reader.read_line(&mut line).map_err(io_to_host)?;
    if read == 0 {
        return Err(Error::HostError("unexpected end of stream".to_owned()));
    }
    Ok(line.trim_end_matches(['\r', '\n']).to_owned())
}