diff --git a/src/imap.rs b/src/imap.rs index e60a980..57d7968 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -1,12 +1,10 @@ use imap; -use mailparse; use native_tls::{self, TlsConnector, TlsStream}; use std::{fmt, net::TcpStream, result}; use crate::config; use crate::email::{self, Email}; use crate::mailbox::Mailbox; -use crate::msg::Msg; // Error wrapper @@ -61,13 +59,13 @@ type Result = result::Result; // Imap connector #[derive(Debug)] -pub struct ImapConnector { - pub config: config::ServerInfo, +pub struct ImapConnector<'a> { + pub config: &'a config::ServerInfo, pub sess: imap::Session>, } -impl ImapConnector { - pub fn new(config: config::ServerInfo) -> Result { +impl<'a> ImapConnector<'a> { + pub fn new(config: &'a config::ServerInfo) -> Result { let tls = TlsConnector::new()?; let client = imap::connect(config.get_addr(), &config.host, &tls)?; let sess = client @@ -133,9 +131,18 @@ impl ImapConnector { } } - pub fn append_msg(&mut self, mbox: &str, msg: &Msg) -> Result<()> { + pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result> { + self.sess.select(mbox)?; + + match self.sess.uid_fetch(uid, "BODY[]")?.first() { + None => Err(Error::ReadEmailNotFoundError(uid.to_string())), + Some(fetch) => Ok(fetch.body().unwrap_or(&[]).to_vec()), + } + } + + pub fn append_msg(&mut self, mbox: &str, msg: &[u8]) -> Result<()> { use imap::types::Flag::*; - self.sess.append_with_flags(mbox, msg.to_vec(), &[Seen])?; + self.sess.append_with_flags(mbox, msg, &[Seen])?; Ok(()) } } diff --git a/src/input.rs b/src/input.rs index 0443844..02f7d19 100644 --- a/src/input.rs +++ b/src/input.rs @@ -7,8 +7,6 @@ use std::{ result, }; -use crate::config::Config; - // Error wrapper #[derive(Debug)] @@ -39,7 +37,7 @@ type Result = result::Result; // Utils -fn open_editor_with_tpl(tpl: &[u8]) -> Result { +pub fn open_editor_with_tpl(tpl: &[u8]) -> Result { // Creates draft file let mut draft_path = temp_dir(); draft_path.push("himalaya-draft.mail"); @@ -56,15 +54,6 @@ fn open_editor_with_tpl(tpl: &[u8]) -> Result { Ok(draft) } -pub fn open_editor_with_new_tpl(config: &Config) -> Result { - let from = &format!("From: {}", config.email_full()); - let to = "To: "; - let subject = "Subject: "; - let headers = [from, to, subject, ""].join("\r\n"); - - Ok(open_editor_with_tpl(headers.as_bytes())?) -} - pub fn ask_for_confirmation(prompt: &str) -> Result<()> { print!("{} (y/n) ", prompt); io::stdout().flush()?; diff --git a/src/main.rs b/src/main.rs index 2ca92da..b5584c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,6 +78,7 @@ fn mailbox_arg() -> Arg<'static, 'static> { .long("mailbox") .help("Name of the targeted mailbox") .value_name("STRING") + .default_value("INBOX") } fn uid_arg() -> Arg<'static, 'static> { @@ -97,7 +98,7 @@ fn run() -> Result<()> { .subcommand( SubCommand::with_name("search") .about("Lists emails matching the given IMAP query") - .arg(mailbox_arg().default_value("INBOX")) + .arg(mailbox_arg()) .arg( Arg::with_name("query") .help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)") @@ -110,7 +111,7 @@ fn run() -> Result<()> { SubCommand::with_name("read") .about("Reads an email by its UID") .arg(uid_arg()) - .arg(mailbox_arg().default_value("INBOX")) + .arg(mailbox_arg()) .arg( Arg::with_name("mime-type") .help("MIME type to use") @@ -126,7 +127,7 @@ fn run() -> Result<()> { SubCommand::with_name("reply") .about("Replies to an email by its UID") .arg(uid_arg()) - .arg(mailbox_arg().default_value("INBOX")) + .arg(mailbox_arg()) .arg( Arg::with_name("reply all") .help("Replies to all recipients") @@ -138,13 +139,13 @@ fn run() -> Result<()> { SubCommand::with_name("forward") .about("Forwards an email by its UID") .arg(uid_arg()) - .arg(mailbox_arg().default_value("INBOX")), + .arg(mailbox_arg()), ) .get_matches(); if let Some(_) = matches.subcommand_matches("list") { let config = Config::new_from_file()?; - let mboxes = ImapConnector::new(config.imap)?.list_mboxes()?.to_table(); + let mboxes = ImapConnector::new(&config.imap)?.list_mboxes()?.to_table(); println!("{}", mboxes); } @@ -177,7 +178,7 @@ fn run() -> Result<()> { .1 .join(" "); - let emails = ImapConnector::new(config.imap)? + let emails = ImapConnector::new(&config.imap)? .read_emails(&mbox, &query)? .to_table(); @@ -190,30 +191,71 @@ fn run() -> Result<()> { let mbox = matches.value_of("mailbox").unwrap(); let uid = matches.value_of("uid").unwrap(); let mime = matches.value_of("mime-type").unwrap(); - let body = ImapConnector::new(config.imap)?.read_email_body(&mbox, &uid, &mime)?; + let body = ImapConnector::new(&config.imap)?.read_email_body(&mbox, &uid, &mime)?; println!("{}", body); } if let Some(_) = matches.subcommand_matches("write") { let config = Config::new_from_file()?; - let content = input::open_editor_with_new_tpl(&config)?; - let msg = Msg::from_raw(content.as_bytes())?; + let mut imap_conn = ImapConnector::new(&config.imap)?; + let tpl = Msg::build_new_tpl(&config)?; + let content = input::open_editor_with_tpl(&tpl.as_bytes())?; + let msg = Msg::from(content.as_bytes())?; - input::ask_for_confirmation("Would you like to send this email?")?; + input::ask_for_confirmation("Send the message?")?; println!("Sending …"); - smtp::send(&config.smtp, &msg)?; - ImapConnector::new(config.imap)?.append_msg("Sent", &msg)?; - println!("Sent!"); + smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; + imap_conn.append_msg("Sent", &msg.to_vec()?)?; + println!("Done!"); } - if let Some(_) = matches.subcommand_matches("reply") { - // TODO + if let Some(matches) = matches.subcommand_matches("reply") { + let config = Config::new_from_file()?; + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + let mut imap_conn = ImapConnector::new(&config.imap)?; + + let msg = imap_conn.read_msg(&mbox, &uid)?; + let msg = Msg::from(&msg)?; + + let tpl = if matches.is_present("reply all") { + msg.build_reply_all_tpl(&config)? + } else { + msg.build_reply_tpl(&config)? + }; + + let content = input::open_editor_with_tpl(&tpl.as_bytes())?; + let msg = Msg::from(content.as_bytes())?; + + input::ask_for_confirmation("Send the message?")?; + + println!("Sending …"); + smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; + imap_conn.append_msg("Sent", &msg.to_vec()?)?; + println!("Done!"); } - if let Some(_) = matches.subcommand_matches("forward") { - // TODO + if let Some(matches) = matches.subcommand_matches("forward") { + let config = Config::new_from_file()?; + let mbox = matches.value_of("mailbox").unwrap(); + let uid = matches.value_of("uid").unwrap(); + let mut imap_conn = ImapConnector::new(&config.imap)?; + + let msg = imap_conn.read_msg(&mbox, &uid)?; + let msg = Msg::from(&msg)?; + + let tpl = msg.build_forward_tpl(&config)?; + let content = input::open_editor_with_tpl(&tpl.as_bytes())?; + let msg = Msg::from(content.as_bytes())?; + + input::ask_for_confirmation("Send the message?")?; + + println!("Sending …"); + smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; + imap_conn.append_msg("Sent", &msg.to_vec()?)?; + println!("Done!"); } Ok(()) diff --git a/src/msg.rs b/src/msg.rs index 806605b..be6dca0 100644 --- a/src/msg.rs +++ b/src/msg.rs @@ -1,6 +1,8 @@ use lettre; -use mailparse; -use std::{fmt, result}; +use mailparse::{self, MailHeaderMap}; +use std::{fmt, ops, result}; + +use crate::Config; // Error wrapper @@ -8,6 +10,7 @@ use std::{fmt, result}; pub enum Error { ParseMsgError(mailparse::MailParseError), BuildEmailError(lettre::error::Error), + TryError, } impl fmt::Display for Error { @@ -16,6 +19,7 @@ impl fmt::Display for Error { match self { Error::ParseMsgError(err) => err.fmt(f), Error::BuildEmailError(err) => err.fmt(f), + Error::TryError => write!(f, "cannot parse"), } } } @@ -38,30 +42,68 @@ type Result = result::Result; // Wrapper around mailparse::ParsedMail and lettre::Message -pub struct Msg(lettre::Message); +#[derive(Debug)] +pub struct Msg<'a>(mailparse::ParsedMail<'a>); -impl Msg { - pub fn from_raw(bytes: &[u8]) -> Result { +impl<'a> ops::Deref for Msg<'a> { + type Target = mailparse::ParsedMail<'a>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> Msg<'a> { + pub fn from(bytes: &'a [u8]) -> Result { + Ok(Self(mailparse::parse_mail(bytes)?)) + } + + pub fn to_vec(&self) -> Result> { + let headers = self.0.get_headers().get_raw_bytes().to_vec(); + let sep = "\r\n".as_bytes().to_vec(); + let body = self.0.get_body()?.as_bytes().to_vec(); + + Ok(vec![headers, sep, body].concat()) + } + + pub fn to_sendable_msg(&self) -> Result { use lettre::message::header::{ContentTransferEncoding, ContentType}; use lettre::message::{Message, SinglePart}; - let parsed_msg = mailparse::parse_mail(bytes)?; - let built_msg = parsed_msg + let msg = self + .0 .headers .iter() .fold(Message::builder(), |msg, h| { + let value = String::from_utf8(h.get_value_raw().to_vec()) + .unwrap() + .replace("\r", ""); + match h.get_key().to_lowercase().as_str() { - "from" => msg.from(h.get_value().parse().unwrap()), - "to" => msg.to(h.get_value().parse().unwrap()), - "cc" => match h.get_value().parse() { + "in-reply-to" => msg.in_reply_to(value.parse().unwrap()), + "from" => match value.parse() { + Ok(addr) => msg.from(addr), Err(_) => msg, - Ok(addr) => msg.cc(addr), }, - "bcc" => match h.get_value().parse() { - Err(_) => msg, - Ok(addr) => msg.bcc(addr), - }, - "subject" => msg.subject(h.get_value()), + "to" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.to(addr), + Err(_) => msg, + }), + "cc" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.cc(addr), + Err(_) => msg, + }), + "bcc" => value + .split(",") + .fold(msg, |msg, addr| match addr.trim().parse() { + Ok(addr) => msg.bcc(addr), + Err(_) => msg, + }), + "subject" => msg.subject(value), _ => msg, } }) @@ -69,17 +111,171 @@ impl Msg { SinglePart::builder() .header(ContentType("text/plain; charset=utf-8".parse().unwrap())) .header(ContentTransferEncoding::Base64) - .body(parsed_msg.get_body_raw()?), + .body(self.0.get_body_raw()?), )?; - Ok(Msg(built_msg)) + Ok(msg) } - pub fn as_sendable_msg(&self) -> &lettre::Message { - &self.0 + pub fn build_new_tpl(config: &Config) -> Result { + let mut tpl = vec![]; + + // "From" header + tpl.push(format!("From: {}", config.email_full())); + + // "To" header + tpl.push("To: ".to_string()); + + // "Subject" header + tpl.push("Subject: ".to_string()); + + Ok(tpl.join("\r\n")) } - pub fn to_vec(&self) -> Vec { - self.0.formatted() + pub fn build_reply_tpl(&self, config: &Config) -> Result { + let msg = &self.0; + let headers = msg.get_headers(); + let mut tpl = vec![]; + + // "From" header + tpl.push(format!("From: {}", config.email_full())); + + // "In-Reply-To" header + if let Some(msg_id) = headers.get_first_value("message-id") { + tpl.push(format!("In-Reply-To: {}", msg_id)); + } + + // "To" header + let to = headers + .get_first_value("reply-to") + .or(headers.get_first_value("from")) + .unwrap_or(String::new()); + tpl.push(format!("To: {}", to)); + + // "Subject" header + let subject = headers.get_first_value("subject").unwrap_or(String::new()); + tpl.push(format!("Subject: Re: {}", subject)); + + // Separator between headers and body + tpl.push(String::new()); + + // Original msg prepend with ">" + let thread = msg + .get_body() + .unwrap() + .split("\r\n") + .map(|line| format!(">{}", line)) + .collect::>() + .join("\r\n"); + tpl.push(thread); + + Ok(tpl.join("\r\n")) + } + + pub fn build_reply_all_tpl(&self, config: &Config) -> Result { + let msg = &self.0; + let headers = msg.get_headers(); + let mut tpl = vec![]; + + // "From" header + tpl.push(format!("From: {}", config.email_full())); + + // "In-Reply-To" header + if let Some(msg_id) = headers.get_first_value("message-id") { + tpl.push(format!("In-Reply-To: {}", msg_id)); + } + + // "To" header + // All addresses coming from original "To" … + let email: lettre::Address = config.email.parse().unwrap(); + let to = headers + .get_all_values("to") + .iter() + .flat_map(|addrs| addrs.split(",")) + .fold(vec![], |mut mboxes, addr| { + match addr.trim().parse::() { + Err(_) => mboxes, + Ok(mbox) => { + // … except current user's one (from config) … + if mbox.email != email { + mboxes.push(mbox.to_string()); + } + mboxes + } + } + }); + // … and the ones coming from either "Reply-To" or "From" + let reply_to = headers + .get_all_values("reply-to") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>(); + let reply_to = if reply_to.is_empty() { + headers + .get_all_values("from") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>() + } else { + reply_to + }; + tpl.push(format!("To: {}", vec![reply_to, to].concat().join(", "))); + + // "Cc" header + let cc = headers + .get_all_values("cc") + .iter() + .flat_map(|addrs| addrs.split(",")) + .map(|addr| addr.trim().to_string()) + .collect::>(); + if !cc.is_empty() { + tpl.push(format!("Cc: {}", cc.join(", "))); + } + + // "Subject" header + let subject = headers.get_first_value("subject").unwrap_or(String::new()); + tpl.push(format!("Subject: Re: {}", subject)); + + // Separator between headers and body + tpl.push(String::new()); + + // Original msg prepend with ">" + let thread = msg + .get_body() + .unwrap() + .split("\r\n") + .map(|line| format!(">{}", line)) + .collect::>() + .join("\r\n"); + tpl.push(thread); + + Ok(tpl.join("\r\n")) + } + + pub fn build_forward_tpl(&self, config: &Config) -> Result { + let msg = &self.0; + let headers = msg.get_headers(); + let mut tpl = vec![]; + + // "From" header + tpl.push(format!("From: {}", config.email_full())); + + // "To" header + tpl.push("To: ".to_string()); + + // "Subject" header + let subject = headers.get_first_value("subject").unwrap_or(String::new()); + tpl.push(format!("Subject: Fwd: {}", subject)); + + // Separator between headers and body + tpl.push(String::new()); + + // Original msg + tpl.push("-------- Forwarded Message --------".to_string()); + tpl.push(msg.get_body().unwrap_or(String::new())); + + Ok(tpl.join("\r\n")) } } diff --git a/src/smtp.rs b/src/smtp.rs index 91a9b9e..fb30604 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -2,7 +2,6 @@ use lettre; use std::{fmt, result}; use crate::config; -use crate::msg::Msg; // Error wrapper @@ -32,12 +31,12 @@ type Result = result::Result; // Utils -pub fn send(config: &config::ServerInfo, msg: &Msg) -> Result<()> { +pub fn send(config: &config::ServerInfo, msg: &lettre::Message) -> Result<()> { use lettre::Transport; lettre::transport::smtp::SmtpTransport::relay(&config.host)? .credentials(config.to_smtp_creds()) .build() - .send(msg.as_sendable_msg()) + .send(msg) .map(|_| Ok(()))? }