enqueue-email 0.1.0

Send a bookmark via email, enqueuing it with MSMTP queue
Documentation
use async_std::task;
use async_tls::TlsConnector;
use enqueuemail::connector;
use enqueuemail::page_content;
use enqueuemail::page_title_from_html;
use enqueuemail::Jump;
use enqueuemail::Options;
use enqueuemail::Parts;
use env_logger::Env;
use std::boxed::Box;
use std::convert::TryFrom;
use std::env;
use std::error::Error;
use std::fs::File;
use std::io::stderr;
use std::io::stdout;
use std::io::Write;
use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use structopt::StructOpt;

#[macro_use]
extern crate log;

#[deny(missing_docs, unused_variables)]

const HOME: &str = "HOME";
const QUEUE_DIR: &str = ".msmtp.queue";

// When the host returns a 3XX HTTP StatusCode, follow the redirect no
// more than MAX_REDIRECTS times.
const MAX_REDIRECTS: u8 = 10;

const KEY_ENQUEUE_MAIL_FROM: &str = "ENQUEUE_MAIL_FROM";
const KEY_ENQUEUE_MAIL_TO: &str = "ENQUEUE_MAIL_TO";
const KEY_ENQUEUE_MAIL_CC: &str = "ENQUEUE_MAIL_CC";
const KEY_ENQUEUE_MAIL_FCC: &str = "ENQUEUE_MAIL_FCC";
const KEY_ENQUEUE_MAIL_ACCOUNT: &str = "ENQUEUE_MAIL_ACCOUNT";

/// EnqueueEmail
///
/// Pass an URL using one of the options available (clipboard, full URL or
/// a combination of the URL parts, and enqueue an email, ready to be
/// spooled by msmtp-queue.
///
/// SETUP:
///
/// The following environment variables are required:
///
/// ENQUEUE_MAIL_FROM, ENQUEUE_MAIL_TO, ENQUEUE_MAIL_CC [optional],
/// ENQUEUE_MAIL_FCC [in the format "provider/folder"]
fn main() -> Result<(), Box<dyn Error>> {
    env_logger::from_env(Env::default().default_filter_or("warn")).init();

    let options = Options::from_args();
    let parts: Parts = Parts::try_from(&options)?;
    let connector: TlsConnector = task::block_on(async { connector(&options.cafile).await })?;

    let mut jumps: u8 = MAX_REDIRECTS;
    let pc: Vec<u8> = task::block_on(async {
        let parts: &mut Parts = &mut parts.clone();
        loop {
            match page_content(&connector, &parts).await {
                Err(e) => {
                    panic!(e.to_string());
                }
                Ok(Jump::Next(url)) if jumps != 0 => {
                    jumps -= 1;
                    *parts = Parts::try_from(url.as_str())
                        .expect("could not derive the URL parts from the Redirect location");
                }
                Ok(Jump::Next(_)) => {
                    panic!("too many redirects");
                }
                Ok(Jump::Landing(page)) => break page,
            };
        }
    });

    let page_title: String = match page_title_from_html(&pc) {
        Ok(Some(t)) => t,
        Ok(None) => parts.domain,
        Err(_) => panic!("unparseable page title"),
    };
    info!("page title: {}", &page_title);

    let home: String = match env::var(HOME) {
        Ok(val) => val,
        Err(_) => panic!("$HOME must be defined in the ENV"),
    };

    let filename: String = match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(n) => n.as_secs().to_string(),
        Err(_) => panic!("SystemTime before UNIX EPOCH!"),
    };

    let mail_filename = format!("{}.mail", filename);
    let mail_path = Path::new(&home).join(QUEUE_DIR).join(&mail_filename);

    let mail_from: String = match env::var(KEY_ENQUEUE_MAIL_FROM) {
        Ok(val) => val,
        Err(_) => panic!(format!(
            "missing ENV configuration: {}",
            KEY_ENQUEUE_MAIL_FROM
        )),
    };

    let mail_to: String = match env::var(KEY_ENQUEUE_MAIL_TO) {
        Ok(val) => val,
        Err(_) => panic!(format!(
            "missing ENV configuration: {}",
            KEY_ENQUEUE_MAIL_TO
        )),
    };

    let mail_cc: String = match env::var(KEY_ENQUEUE_MAIL_CC) {
        Ok(val) => val,
        Err(_) => panic!(format!(
            "missing ENV configuration: {}",
            KEY_ENQUEUE_MAIL_CC
        )),
    };

    let mail_fcc: String = match env::var(KEY_ENQUEUE_MAIL_FCC) {
        Ok(val) => val,
        Err(_) => panic!(format!(
            "missing ENV configuration: {}",
            KEY_ENQUEUE_MAIL_FCC
        )),
    };

    // The email account formatted as "-a default user <user@mail.org>\n"
    let mail_account: String = match env::var(KEY_ENQUEUE_MAIL_ACCOUNT) {
        Ok(val) => format!("-a default {}\n", val),
        Err(_) => panic!(format!(
            "missing ENV configuration: {}",
            KEY_ENQUEUE_MAIL_ACCOUNT
        )),
    };

    let mut file = File::create(mail_path)
        .map_err(|e| format!("Problem opening the file {:#?}: {}", &mail_filename, e))?;

    let mail_body: String = format!(
        "From: {}\n\
         To: {}\n\
         Cc: {}\n\
         Subject: [link] {}\n\
         Fcc: {}\n\
         Bcc:\n\
         \n\
         title: {}\n\
         URL: {}\n\
         link: [[{}][{}]]\n\
         ---\n",
        &mail_from,
        &mail_to,
        &mail_cc,
        page_title,
        &mail_fcc,
        page_title,
        &parts.url.as_str(),
        &parts.url.as_str(),
        page_title,
    );

    debug!("mail body: \n{}", &mail_body);
    file.write_all(mail_body.as_bytes())?;

    let msmtp_filename = format!("{}.msmtp", filename);
    let mut file = File::create(Path::new(&home).join(QUEUE_DIR).join(&msmtp_filename))
        .map_err(|e| format!("Problem opening the file {:#?}: {}", &msmtp_filename, e))?;
    file.write_all(&mail_account.into_bytes())?;

    if options.run {
        let output = Command::new("msmtp-queue")
            .args(&["-r"])
            .output()
            .expect("command failed to start");

        return if output.status.success() {
            stdout()
                .write_all(&output.stdout)
                .expect("failed to write on STDOUT");
            Ok(())
        } else {
            stderr()
                .write_all(&output.stderr)
                .expect("failed to write on STDERR");
            let e: Box<dyn Error> = From::from("queue flush failed");
            Err(e)
        };
    }

    Ok(())
}