m2 0.0.0

Set of Unix tools to work with m2dirs
Documentation
mod html;
mod pgp;

use std::{
    error::Error,
    io::{Read, Write, stdin, stdout},
};

use clap::Parser;
use html::generate_html;
use m2::Message;
use mail_builder::{
    MessageBuilder,
    headers::content_type::ContentType,
    mime::{BodyPart, MimePart},
};

#[derive(Debug, Parser)]
#[command(name = "m2proc", author, version)]
pub struct Args {
    /// PGP/MIME-sign email using gpg
    #[arg(long, default_value_t = false)]
    sign: bool,
    /// Don't create a multipart email with generated HTML
    #[arg(long, default_value_t = false)]
    no_html: bool,
}

pub fn preprocess<R: Read, W: Write>(
    mut r: R,
    w: W,
    sign: bool,
    gen_html: bool,
) -> Result<(), Box<dyn Error>> {
    let msg = Message::from_reader(&mut r)?;
    let mut builder = msg.builder();

    if gen_html {
        builder = multipart_html(builder);
    }

    if sign {
        builder = sign_builder(builder)?;
    }

    Ok(builder.write_to(w)?)
}

pub fn multipart_html(mut builder: MessageBuilder) -> MessageBuilder {
    let plain_ctype = ContentType::new("text/plain")
        .attribute("charset", "utf-8")
        .attribute("format", "flowed");
    let html_ctype = ContentType::new("text/html").attribute("charset", "utf-8");

    if let Some(ref mut body) = builder.body {
        let plain_body: String = match &body.contents {
            BodyPart::Text(s) => s.to_string(),
            _ => "".to_string(),
        };

        let html_body = generate_html(plain_body.clone());
        builder.headers.append(&mut body.headers);
        builder = builder.body(MimePart::new(
            "multipart/alternative",
            vec![
                MimePart::new(plain_ctype.clone(), plain_body),
                MimePart::new(html_ctype.clone(), html_body),
            ],
        ));
    }

    builder
}

pub fn sign_builder(msg_builder: MessageBuilder) -> Result<MessageBuilder, Box<dyn Error>> {
    let body = msg_builder.clone().body.ok_or("no body to sign")?;

    let mut mail_bytes = Vec::new();
    body.clone().write_part(&mut mail_bytes)?;

    let mut signature = Vec::<u8>::new();
    pgp::sign(mail_bytes.trim_ascii(), &mut signature)?;

    // NOTE: We are trusting that the output of `gpg --armor` is valid UTF-8
    let signature = MimePart::new(
        "application/pgp-signature",
        String::from_utf8_lossy(&signature).into_owned(),
    )
    .transfer_encoding("7bit")
    .attachment("signature.asc");

    let content_type = ContentType {
        c_type: "multipart/signed".into(),
        attributes: vec![
            ("micalg".into(), "pgp-sha1".into()),
            ("protocol".into(), "application/pgp-signature".into()),
        ],
    };

    Ok(msg_builder.body(MimePart::new(content_type, vec![body, signature])))
}

pub fn main() {
    let args = Args::parse();
    if let Err(e) = preprocess(stdin(), stdout(), args.sign, !args.no_html) {
        eprintln!("{e}");
    }
}