pub mod serde_pid;
mod util;
use crate::util::FlattenResultsIter;
use displaydoc::Display;
use lazy_static::lazy_static;
use nix::unistd::Pid;
use regex::Regex;
use serde_derive::{Deserialize, Serialize};
use std::convert::TryInto;
use std::{env, fmt, fs, io, path, thread, time};
use structopt::{clap, StructOpt};
use thiserror::Error;
pub const ENV_MAIL_STORE_PATH: &str = "TRAPMAIL_STORE";
const DEFAULT_MAIL_STORE_PATH: &str = "/tmp";
lazy_static! {
static ref FILENAME_RE: Regex = Regex::new(r"trapmail_\d+_\d+_\d+.json").unwrap();
}
#[derive(Clone, Debug, Deserialize, Serialize, StructOpt)]
pub struct CliOptions {
#[structopt(long = "debug")]
pub debug: bool,
#[structopt(short = "i")]
pub ignore_dots: bool,
#[structopt(short = "t")]
pub inline_recipients: bool,
pub cli_options: Vec<String>,
#[structopt(long = "dump")]
pub dump: Option<path::PathBuf>,
#[structopt(short = "f")]
pub sender: Option<String>,
#[structopt(long = "store-path")]
pub store_path: Option<String>,
}
impl CliOptions {
pub fn from_args() -> Self {
let app = Self::clap().setting(clap::AppSettings::AllowLeadingHyphen);
Self::from_clap(&app.get_matches())
}
}
#[derive(Debug, Display, Error)]
pub enum Error {
Store(io::Error),
MailSerialization(serde_json::Error),
DirEnumeration(util::DirReadError),
Load(io::Error),
MailDeserialization(serde_json::Error),
}
type Result<T> = ::std::result::Result<T, Error>;
#[derive(Debug, Deserialize, Serialize)]
pub enum MailBody {
Utf8(String),
Invalid(#[serde(with = "serde_bytes")] Vec<u8>),
}
impl MailBody {
#[inline]
fn from_raw(raw_body: Vec<u8>) -> Self {
match String::from_utf8(raw_body) {
Ok(s) => MailBody::Utf8(s),
Err(e) => MailBody::Invalid(e.into_bytes()),
}
}
}
impl fmt::Display for MailBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MailBody::Utf8(s) => write!(f, "{}", s),
MailBody::Invalid(raw) => write!(f, "[invalid UTF-8]{}", String::from_utf8_lossy(&raw)),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Mail {
pub cli_options: CliOptions,
#[serde(with = "serde_pid")]
pub pid: Pid,
#[serde(with = "serde_pid")]
pub ppid: Pid,
pub body: MailBody,
pub timestamp_us: u128,
}
impl Mail {
pub fn new(cli_options: CliOptions, raw_body: Vec<u8>) -> Self {
thread::sleep(time::Duration::from_nanos(1000));
let timestamp_us = (time::SystemTime::now().duration_since(time::UNIX_EPOCH))
.expect("Got current before 1970; is your clock broken?")
.as_micros();
Mail {
cli_options,
body: MailBody::from_raw(raw_body),
pid: nix::unistd::Pid::this(),
ppid: nix::unistd::Pid::parent(),
timestamp_us,
}
}
pub fn file_name(&self) -> path::PathBuf {
format!(
"trapmail_{}_{}_{}.json",
self.timestamp_us, self.ppid, self.pid,
)
.into()
}
pub fn load<P: AsRef<path::Path>>(source: P) -> Result<Self> {
serde_json::from_reader(fs::File::open(source).map_err(Error::Load)?)
.map_err(Error::MailDeserialization)
}
}
fn us_to_datetime(
timestamp_us: u128,
) -> ::core::result::Result<chrono::NaiveDateTime, core::num::TryFromIntError> {
Ok(chrono::NaiveDateTime::from_timestamp(
(timestamp_us / 1_000_000).try_into()?,
(timestamp_us % 1_000_000).try_into()?,
))
}
impl fmt::Display for Mail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let formatted_timestamp = if let Ok(dt) = us_to_datetime(self.timestamp_us) {
dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string()
} else {
format!("[cannot convert {} to timestamp]", self.timestamp_us)
};
write!(
f,
"Mail sent on {} UTC from PID {} (PPID {}).\n\
{:#?}\n\
{}",
formatted_timestamp, self.pid, self.ppid, self.cli_options, self.body
)
}
}
#[derive(Debug)]
pub struct MailStore {
root: path::PathBuf,
}
impl MailStore {
#[inline]
pub fn new() -> Self {
MailStore::with_root(
env::var(ENV_MAIL_STORE_PATH).unwrap_or_else(|_| DEFAULT_MAIL_STORE_PATH.to_owned()),
)
}
#[inline]
pub fn with_root<P: Into<path::PathBuf>>(root: P) -> Self {
MailStore { root: root.into() }
}
pub fn add(&self, mail: &Mail) -> Result<path::PathBuf> {
let output_fn = self.root.join(mail.file_name());
serde_json::to_writer_pretty(fs::File::create(&output_fn).map_err(Error::Store)?, mail)
.map_err(Error::MailSerialization)?;
Ok(output_fn)
}
pub fn iter_mails(&self) -> impl Iterator<Item = Result<Mail>> {
util::read_dir_matching(&self.root, &FILENAME_RE)
.map_err(Error::DirEnumeration)
.map(|paths| paths.into_iter().map(Mail::load))
.flatten_results()
}
}
impl Default for MailStore {
fn default() -> Self {
Self::new()
}
}