mirror of
https://github.com/soywod/himalaya.git
synced 2024-09-20 17:01:27 +00:00
implement download attachments feature
This commit is contained in:
parent
6f7ee69cfe
commit
1536fdb894
|
@ -18,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Text and HTML previews [#12] [#13]
|
||||
- Set up SMTP connection [#4]
|
||||
- Write new email [#8]
|
||||
- Write new email [#8]
|
||||
- Reply, reply all and forward [#9] [#10] [#11]
|
||||
- Download attachments [#14]
|
||||
|
||||
[unreleased]: https://github.com/soywod/himalaya
|
||||
|
||||
|
@ -27,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#4]: https://github.com/soywod/himalaya/issues/4
|
||||
[#5]: https://github.com/soywod/himalaya/issues/5
|
||||
[#8]: https://github.com/soywod/himalaya/issues/8
|
||||
[#9]: https://github.com/soywod/himalaya/issues/9
|
||||
[#10]: https://github.com/soywod/himalaya/issues/10
|
||||
[#11]: https://github.com/soywod/himalaya/issues/11
|
||||
[#12]: https://github.com/soywod/himalaya/issues/12
|
||||
[#13]: https://github.com/soywod/himalaya/issues/13
|
||||
[#14]: https://github.com/soywod/himalaya/issues/14
|
||||
[#15]: https://github.com/soywod/himalaya/issues/15
|
||||
|
|
|
@ -19,11 +19,12 @@ FLAGS:
|
|||
-V, --version Prints version information
|
||||
|
||||
SUBCOMMANDS:
|
||||
forward Forwards an email by its UID
|
||||
attachments Downloads all attachments from an email
|
||||
forward Forwards an email
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
list Lists all available mailboxes
|
||||
read Reads an email by its UID
|
||||
reply Replies to an email by its UID
|
||||
read Reads text bodies of an email
|
||||
reply Answers to an email
|
||||
search Lists emails matching the given IMAP query
|
||||
write Writes a new email
|
||||
```
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
53
src/main.rs
53
src/main.rs
|
@ -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
src/msg.rs
34
src/msg.rs
|
@ -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![];
|
||||
|
||||
|
|
Loading…
Reference in a new issue