implement download attachments feature

This commit is contained in:
Clément DOUIN
2021-01-15 12:21:07 +01:00
parent 6f7ee69cfe
commit 1536fdb894
7 changed files with 112 additions and 29 deletions
+15 -7
View File
@@ -77,6 +77,7 @@ impl ServerInfo {
pub struct Config {
pub name: String,
pub email: String,
pub downloads_dir: Option<PathBuf>,
pub imap: ServerInfo,
pub smtp: ServerInfo,
}
@@ -91,7 +92,7 @@ impl Config {
Ok(path)
}
fn path_from_home(_err: Error) -> Result<PathBuf> {
fn path_from_home() -> Result<PathBuf> {
let path = env::var("HOME")?;
let mut path = PathBuf::from(path);
path.push(".config");
@@ -101,7 +102,7 @@ impl Config {
Ok(path)
}
fn path_from_tmp(_err: Error) -> Result<PathBuf> {
fn path_from_tmp() -> Result<PathBuf> {
let mut path = env::temp_dir();
path.push("himalaya");
path.push("config.toml");
@@ -112,18 +113,25 @@ impl Config {
pub fn new_from_file() -> Result<Self> {
let mut file = File::open(
Self::path_from_xdg()
.or_else(Self::path_from_home)
.or_else(Self::path_from_tmp)
.or_else(|_| Self::path_from_home())
.or_else(|_| Self::path_from_tmp())
.or_else(|_| Err(Error::GetPathNotFoundError))?,
)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let mut content = vec![];
file.read_to_end(&mut content)?;
Ok(toml::from_str(&content)?)
Ok(toml::from_slice(&content)?)
}
pub fn email_full(&self) -> String {
format!("{} <{}>", self.name, self.email)
}
pub fn downloads_filepath(&self, filename: &str) -> PathBuf {
let temp_dir = env::temp_dir();
let mut full_path = self.downloads_dir.as_ref().unwrap_or(&temp_dir).to_owned();
full_path.push(filename);
full_path
}
}
+3 -3
View File
@@ -205,7 +205,7 @@ fn extract_text_bodies_into(mime: &str, part: &mailparse::ParsedMail, parts: &mu
if part
.get_headers()
.get_first_value("content-type")
.and_then(|v| if v.starts_with(mime) { Some(()) } else { None })
.and_then(|v| if v.starts_with(&mime) { Some(()) } else { None })
.is_some()
{
parts.push(part.get_body().unwrap_or(String::new()))
@@ -214,13 +214,13 @@ fn extract_text_bodies_into(mime: &str, part: &mailparse::ParsedMail, parts: &mu
_ => {
part.subparts
.iter()
.for_each(|part| extract_text_bodies_into(mime, part, parts));
.for_each(|part| extract_text_bodies_into(&mime, part, parts));
}
}
}
pub fn extract_text_bodies(mime: &str, email: &mailparse::ParsedMail) -> String {
let mut parts = vec![];
extract_text_bodies_into(mime, email, &mut parts);
extract_text_bodies_into(&mime, email, &mut parts);
parts.join("\r\n")
}
+4
View File
@@ -15,6 +15,7 @@ pub enum Error {
ParseEmailError(mailparse::MailParseError),
ReadEmailNotFoundError(String),
ReadEmailEmptyPartError(String, String),
ExtractAttachmentsEmptyError(String),
}
impl fmt::Display for Error {
@@ -30,6 +31,9 @@ impl fmt::Display for Error {
Error::ReadEmailEmptyPartError(uid, mime) => {
write!(f, "no {} content found for uid {}", mime, uid)
}
Error::ExtractAttachmentsEmptyError(uid) => {
write!(f, "no attachment found for uid {}", uid)
}
}
}
}
+41 -12
View File
@@ -8,7 +8,7 @@ mod smtp;
mod table;
use clap::{App, AppSettings, Arg, SubCommand};
use std::{fmt, process::exit, result};
use std::{fmt, fs, process::exit, result};
use crate::config::Config;
use crate::imap::ImapConnector;
@@ -76,14 +76,14 @@ fn mailbox_arg() -> Arg<'static, 'static> {
Arg::with_name("mailbox")
.short("m")
.long("mailbox")
.help("Name of the targeted mailbox")
.help("Name of the mailbox")
.value_name("STRING")
.default_value("INBOX")
}
fn uid_arg() -> Arg<'static, 'static> {
Arg::with_name("uid")
.help("UID of the targeted email")
.help("UID of the email")
.value_name("UID")
.required(true)
}
@@ -109,7 +109,7 @@ fn run() -> Result<()> {
)
.subcommand(
SubCommand::with_name("read")
.about("Reads an email by its UID")
.about("Reads text bodies of an email")
.arg(uid_arg())
.arg(mailbox_arg())
.arg(
@@ -118,26 +118,32 @@ fn run() -> Result<()> {
.short("t")
.long("mime-type")
.value_name("STRING")
.possible_values(&["text/plain", "text/html"])
.default_value("text/plain"),
.possible_values(&["plain", "html"])
.default_value("plain"),
),
)
.subcommand(
SubCommand::with_name("attachments")
.about("Downloads all attachments from an email")
.arg(uid_arg())
.arg(mailbox_arg()),
)
.subcommand(SubCommand::with_name("write").about("Writes a new email"))
.subcommand(
SubCommand::with_name("reply")
.about("Replies to an email by its UID")
.about("Answers to an email")
.arg(uid_arg())
.arg(mailbox_arg())
.arg(
Arg::with_name("reply all")
.help("Replies to all recipients")
Arg::with_name("reply-all")
.help("Including all recipients")
.short("a")
.long("all"),
),
)
.subcommand(
SubCommand::with_name("forward")
.about("Forwards an email by its UID")
.about("Forwards an email")
.arg(uid_arg())
.arg(mailbox_arg()),
)
@@ -190,12 +196,35 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mime = matches.value_of("mime-type").unwrap();
let mime = format!("text/{}", matches.value_of("mime-type").unwrap());
let body = ImapConnector::new(&config.imap)?.read_email_body(&mbox, &uid, &mime)?;
println!("{}", body);
}
if let Some(matches) = matches.subcommand_matches("attachments") {
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 parts = msg.extract_parts()?;
if parts.is_empty() {
println!("No attachment found for message {}", uid);
} else {
println!("{} attachment(s) found for message {}", parts.len(), uid);
msg.extract_parts()?.iter().for_each(|(filename, bytes)| {
let filepath = config.downloads_filepath(&filename);
println!("Downloading {}", filename);
fs::write(filepath, bytes).unwrap()
});
println!("Done!");
}
}
if let Some(_) = matches.subcommand_matches("write") {
let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
@@ -220,7 +249,7 @@ fn run() -> Result<()> {
let msg = imap_conn.read_msg(&mbox, &uid)?;
let msg = Msg::from(&msg)?;
let tpl = if matches.is_present("reply all") {
let tpl = if matches.is_present("reply-all") {
msg.build_reply_all_tpl(&config)?
} else {
msg.build_reply_tpl(&config)?
+34
View File
@@ -117,6 +117,40 @@ impl<'a> Msg<'a> {
Ok(msg)
}
fn extract_parts_into(part: &mailparse::ParsedMail, parts: &mut Vec<(String, Vec<u8>)>) {
match part.subparts.len() {
0 => {
let content_disp = part.get_content_disposition();
let content_type = part
.get_headers()
.get_first_value("content-type")
.unwrap_or_default();
let default_attachment_name = format!("attachment-{}", parts.len());
let attachment_name = content_disp
.params
.get("filename")
.unwrap_or(&default_attachment_name)
.to_owned();
if !content_type.starts_with("text") {
parts.push((attachment_name, part.get_body_raw().unwrap_or_default()))
}
}
_ => {
part.subparts
.iter()
.for_each(|part| Self::extract_parts_into(part, parts));
}
}
}
pub fn extract_parts(&self) -> Result<Vec<(String, Vec<u8>)>> {
let mut parts = vec![];
Self::extract_parts_into(&self.0, &mut parts);
Ok(parts)
}
pub fn build_new_tpl(config: &Config) -> Result<String> {
let mut tpl = vec![];