implement reply, reply all and forward features

This commit is contained in:
Clément DOUIN 2021-01-15 00:00:31 +01:00
parent 2709faf30a
commit 43c35532be
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
5 changed files with 295 additions and 62 deletions

View file

@ -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<T> = result::Result<T, Error>;
// Imap connector
#[derive(Debug)]
pub struct ImapConnector {
pub config: config::ServerInfo,
pub struct ImapConnector<'a> {
pub config: &'a config::ServerInfo,
pub sess: imap::Session<TlsStream<TcpStream>>,
}
impl ImapConnector {
pub fn new(config: config::ServerInfo) -> Result<Self> {
impl<'a> ImapConnector<'a> {
pub fn new(config: &'a config::ServerInfo) -> Result<Self> {
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<Vec<u8>> {
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(())
}
}

View file

@ -7,8 +7,6 @@ use std::{
result,
};
use crate::config::Config;
// Error wrapper
#[derive(Debug)]
@ -39,7 +37,7 @@ type Result<T> = result::Result<T, Error>;
// Utils
fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> {
pub fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> {
// 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<String> {
Ok(draft)
}
pub fn open_editor_with_new_tpl(config: &Config) -> Result<String> {
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()?;

View file

@ -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(())

View file

@ -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<T> = result::Result<T, Error>;
// 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<Msg> {
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<Self> {
Ok(Self(mailparse::parse_mail(bytes)?))
}
pub fn to_vec(&self) -> Result<Vec<u8>> {
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<lettre::Message> {
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<String> {
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<u8> {
self.0.formatted()
pub fn build_reply_tpl(&self, config: &Config) -> Result<String> {
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::<Vec<String>>()
.join("\r\n");
tpl.push(thread);
Ok(tpl.join("\r\n"))
}
pub fn build_reply_all_tpl(&self, config: &Config) -> Result<String> {
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::<lettre::message::Mailbox>() {
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::<Vec<String>>();
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::<Vec<String>>()
} 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::<Vec<String>>();
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::<Vec<String>>()
.join("\r\n");
tpl.push(thread);
Ok(tpl.join("\r\n"))
}
pub fn build_forward_tpl(&self, config: &Config) -> Result<String> {
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"))
}
}

View file

@ -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<T> = result::Result<T, Error>;
// 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(()))?
}