m2 0.0.0

Set of Unix tools to work with m2dirs
Documentation
pub mod format;
pub mod table;
pub mod utils;

use std::{
    io::{self, Read},
    path::{Path, PathBuf},
};

use mail_builder::{
    MessageBuilder,
    headers::{
        HeaderType, address::Address as BuilderAddress, content_type::ContentType, date::Date,
        text::Text,
    },
    mime::MimePart,
};
use mail_parser::{
    Addr, Address, Group, HeaderValue, Message as ParsedMessage, MessageParser, MessagePart,
    PartType,
};

pub struct Message {
    inner: ParsedMessage<'static>,
    path: Option<PathBuf>,
}

impl Message {
    pub fn from_path(path: impl AsRef<Path>) -> io::Result<Self> {
        let path = path.as_ref().to_path_buf();
        let bytes = std::fs::read(&path)?;
        Ok(Self {
            inner: MessageParser::new().parse(&bytes).unwrap().into_owned(),
            path: Some(path),
        })
    }

    pub fn from_reader<R: Read>(mut r: R) -> io::Result<Self> {
        let mut bytes = Vec::new();
        r.read_to_end(&mut bytes)?;
        Ok(Self {
            inner: MessageParser::new().parse(&bytes).unwrap().into_owned(),
            path: None,
        })
    }

    pub fn path(&self) -> &Path {
        self.path.as_ref().unwrap()
    }

    pub fn message(&self) -> &ParsedMessage<'_> {
        &self.inner
    }

    pub fn builder(&self) -> MessageBuilder<'_> {
        let mut builder = MessageBuilder::new();
        builder = builder.body(message_part_to_mime_part(
            self.inner.root_part(),
            &self.inner,
        ));
        builder
    }
}

fn message_part_to_mime_part<'a>(part: &'a MessagePart, parsed: &'a ParsedMessage) -> MimePart<'a> {
    let mut mime_part = match &part.body {
        PartType::Text(s) | PartType::Html(s) => MimePart::raw(s.as_ref()),
        PartType::Binary(b) | PartType::InlineBinary(b) => MimePart::raw(b.as_ref()),
        PartType::Multipart(idxs) => MimePart::raw(
            idxs.iter()
                .filter_map(|i| parsed.part(*i))
                .map(|part| message_part_to_mime_part(part, parsed))
                .collect::<Vec<MimePart>>(),
        ),
        // NOTE: mail-builder does not seem to support nested message. The below will likely handle
        // nested messages as a multipart component (I'm not sure.) Since nested messages are so
        // uncommon, I'll allow it here for the sake of handling all variants of PartType.
        PartType::Message(m) => MimePart::raw(
            m.parts
                .iter()
                .map(|part| message_part_to_mime_part(part, parsed))
                .collect::<Vec<MimePart>>(),
        ),
    };

    for header in part.headers() {
        if header.name.as_str() == "MIME-Version" {
            continue;
        }

        mime_part = mime_part.header(
            header.name.as_str(),
            parsed_header_to_builder_header(&header.value),
        );
    }

    mime_part
}

fn parsed_header_to_builder_header<'a>(header_value: &'a HeaderValue) -> HeaderType<'a> {
    match header_value {
        HeaderValue::ContentType(ct) => {
            let mut content_type = ContentType::new(ct.c_type.as_ref());
            if let Some(attributes) = ct.attributes() {
                for attribute in attributes {
                    content_type =
                        content_type.attribute(attribute.name.clone(), attribute.value.clone());
                }
            }
            HeaderType::ContentType(content_type)
        }
        HeaderValue::Text(s) => HeaderType::Text(Text::new(s.as_ref())),
        HeaderValue::DateTime(dt) => HeaderType::Date(Date::new(dt.to_timestamp())),
        HeaderValue::Address(address) => {
            HeaderType::Address(parse_address_to_builder_address(address))
        }
        _ => HeaderType::Text(Text::new("")),
    }
}

fn parse_address_to_builder_address<'a>(addr: &'a Address) -> BuilderAddress<'a> {
    match addr {
        Address::List(addrs) => {
            BuilderAddress::new_list(addrs.iter().map(parse_addr_to_builder_address).collect())
        }
        Address::Group(groups) => {
            BuilderAddress::new_list(groups.iter().map(parse_group_to_builder_address).collect())
        }
    }
}

fn parse_group_to_builder_address<'a>(group: &'a Group) -> BuilderAddress<'a> {
    BuilderAddress::new_group(
        group.name.as_deref(),
        group
            .addresses
            .iter()
            .map(parse_addr_to_builder_address)
            .collect(),
    )
}

fn parse_addr_to_builder_address<'a>(addr: &'a Addr) -> BuilderAddress<'a> {
    BuilderAddress::new_address(
        addr.name.as_deref(),
        addr.address.as_deref().unwrap_or_default(),
    )
}