mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-16 22:33:36 +00:00
implement reply, reply all and forward features
This commit is contained in:
parent
2709faf30a
commit
43c35532be
5 changed files with 295 additions and 62 deletions
23
src/imap.rs
23
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<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(())
|
||||
}
|
||||
}
|
||||
|
|
13
src/input.rs
13
src/input.rs
|
@ -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()?;
|
||||
|
|
76
src/main.rs
76
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(())
|
||||
|
|
240
src/msg.rs
240
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<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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(()))?
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue