use std::{io, process};
#[macro_use]
extern crate lazy_static;
lazy_static! {
static ref OUTLOOK_EXE: Option<&'static str> = {
use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey};
const OUTLOOK_SUBKEY: &str =
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\OUTLOOK.EXE";
let subkey = match RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey(OUTLOOK_SUBKEY) {
Ok(subkey) => subkey,
Err(_) => return None,
};
let value: String = match subkey.get_value("") {
Ok(value) => value,
Err(_) => return None,
};
Some(Box::leak(value.into_boxed_str()))
};
}
fn percent_escape(s: &str) -> String {
s.replace('%', "%25") .replace('"', "%22")
.replace('&', "%26")
.replace('?', "%3F")
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MessageBuilder {
subj: String,
to: Vec<String>,
cc: Vec<String>,
bcc: Vec<String>,
body: String,
file: String,
}
impl MessageBuilder {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
subj: String::new(),
to: Vec::new(),
cc: Vec::new(),
bcc: Vec::new(),
body: String::new(),
file: String::new(),
}
}
#[inline]
#[must_use]
pub fn with_subject<S>(self, subj: S) -> Self
where
S: Into<String>,
{
debug_assert!(self.subj.is_empty(), "Outlook subject already provided");
Self {
subj: subj.into(),
to: self.to,
cc: self.cc,
bcc: self.bcc,
body: self.body,
file: self.file,
}
}
#[inline]
#[must_use]
pub fn with_recipient<S>(mut self, to: S) -> Self
where
S: Into<String>,
{
self.to.push(to.into());
Self {
subj: self.subj,
to: self.to,
cc: self.cc,
bcc: self.bcc,
body: self.body,
file: self.file,
}
}
#[inline]
#[must_use]
pub fn with_recipient_cc<S>(mut self, cc: S) -> Self
where
S: Into<String>,
{
self.cc.push(cc.into());
Self {
subj: self.subj,
to: self.to,
cc: self.cc,
bcc: self.bcc,
body: self.body,
file: self.file,
}
}
#[inline]
#[must_use]
pub fn with_recipient_bcc<S>(mut self, bcc: S) -> Self
where
S: Into<String>,
{
self.bcc.push(bcc.into());
Self {
subj: self.subj,
to: self.to,
cc: self.cc,
bcc: self.bcc,
body: self.body,
file: self.file,
}
}
#[inline]
#[must_use]
pub fn with_body<S>(self, body: S) -> Self
where
S: Into<String>,
{
debug_assert!(self.body.is_empty(), "Outlook body already provided");
Self {
subj: self.subj,
to: self.to,
cc: self.cc,
bcc: self.bcc,
body: body.into(),
file: self.file,
}
}
#[inline]
#[must_use]
pub fn with_attachment<S>(self, file: S) -> Self
where
S: Into<String>,
{
debug_assert!(
self.file.is_empty(),
"Outlook's invocation switches do not support attaching multiple files"
);
Self {
subj: self.subj,
to: self.to,
cc: self.cc,
bcc: self.bcc,
body: self.body,
file: file.into(),
}
}
pub fn spawn(mut self) -> io::Result<process::Child> {
let mut s = String::new();
s.push_str(&percent_escape(&self.to.join(";")));
if !self.cc.is_empty() {
if !s.is_empty() {
s.push('&')
}
s.push_str("cc=");
s.push_str(&percent_escape(&self.cc.join(";")));
}
if !self.bcc.is_empty() {
if !s.is_empty() {
s.push('&')
}
s.push_str("bcc=");
s.push_str(&percent_escape(&self.bcc.join(";")));
}
if !self.subj.is_empty() {
if !s.is_empty() {
s.push('&')
}
s.push_str("subject=");
s.push_str(&percent_escape(&self.subj));
}
if !self.body.is_empty() {
if !s.is_empty() {
s.push('&')
}
s.push_str("body=");
s.push_str(&percent_escape(&self.body));
}
let mut a = Vec::new();
if !self.file.is_empty() {
a.push("/a");
self.file = percent_escape(&self.file);
a.push(&self.file);
}
let outlook_exe =
OUTLOOK_EXE.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "OUTLOOK.EXE"))?;
process::Command::new(outlook_exe)
.arg("/c")
.arg("ipm.note")
.arg("/m")
.arg(s)
.args(a)
.spawn()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_builder() {
let mb = MessageBuilder::new();
assert_eq!(mb.to.len(), 0);
assert_eq!(mb.cc.len(), 0);
assert_eq!(mb.bcc.len(), 0);
assert_eq!(mb.subj, "");
assert_eq!(mb.body, "");
assert_eq!(mb.file, "");
let mb = mb
.with_recipient("noreply@example.org")
.with_subject("Hello, World!")
.with_body("Line with spaces\nAnother line")
.with_attachment("C:/tmp/file.txt");
assert_eq!(mb.to.len(), 1);
assert_eq!(mb.cc.len(), 0);
assert_eq!(mb.bcc.len(), 0);
assert_eq!(mb.to[0], "noreply@example.org");
assert_eq!(mb.subj, "Hello, World!");
assert_eq!(mb.body, "Line with spaces\nAnother line");
assert_eq!(mb.file, "C:/tmp/file.txt");
}
}