mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-21 15:21:13 +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]
|
- Text and HTML previews [#12] [#13]
|
||||||
- Set up SMTP connection [#4]
|
- Set up SMTP connection [#4]
|
||||||
- Write new email [#8]
|
- 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
|
[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
|
[#4]: https://github.com/soywod/himalaya/issues/4
|
||||||
[#5]: https://github.com/soywod/himalaya/issues/5
|
[#5]: https://github.com/soywod/himalaya/issues/5
|
||||||
[#8]: https://github.com/soywod/himalaya/issues/8
|
[#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
|
[#12]: https://github.com/soywod/himalaya/issues/12
|
||||||
[#13]: https://github.com/soywod/himalaya/issues/13
|
[#13]: https://github.com/soywod/himalaya/issues/13
|
||||||
|
[#14]: https://github.com/soywod/himalaya/issues/14
|
||||||
[#15]: https://github.com/soywod/himalaya/issues/15
|
[#15]: https://github.com/soywod/himalaya/issues/15
|
||||||
|
|
|
@ -19,11 +19,12 @@ FLAGS:
|
||||||
-V, --version Prints version information
|
-V, --version Prints version information
|
||||||
|
|
||||||
SUBCOMMANDS:
|
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)
|
help Prints this message or the help of the given subcommand(s)
|
||||||
list Lists all available mailboxes
|
list Lists all available mailboxes
|
||||||
read Reads an email by its UID
|
read Reads text bodies of an email
|
||||||
reply Replies to an email by its UID
|
reply Answers to an email
|
||||||
search Lists emails matching the given IMAP query
|
search Lists emails matching the given IMAP query
|
||||||
write Writes a new email
|
write Writes a new email
|
||||||
```
|
```
|
||||||
|
|
|
@ -77,6 +77,7 @@ impl ServerInfo {
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub downloads_dir: Option<PathBuf>,
|
||||||
pub imap: ServerInfo,
|
pub imap: ServerInfo,
|
||||||
pub smtp: ServerInfo,
|
pub smtp: ServerInfo,
|
||||||
}
|
}
|
||||||
|
@ -91,7 +92,7 @@ impl Config {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_from_home(_err: Error) -> Result<PathBuf> {
|
fn path_from_home() -> Result<PathBuf> {
|
||||||
let path = env::var("HOME")?;
|
let path = env::var("HOME")?;
|
||||||
let mut path = PathBuf::from(path);
|
let mut path = PathBuf::from(path);
|
||||||
path.push(".config");
|
path.push(".config");
|
||||||
|
@ -101,7 +102,7 @@ impl Config {
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_from_tmp(_err: Error) -> Result<PathBuf> {
|
fn path_from_tmp() -> Result<PathBuf> {
|
||||||
let mut path = env::temp_dir();
|
let mut path = env::temp_dir();
|
||||||
path.push("himalaya");
|
path.push("himalaya");
|
||||||
path.push("config.toml");
|
path.push("config.toml");
|
||||||
|
@ -112,18 +113,25 @@ impl Config {
|
||||||
pub fn new_from_file() -> Result<Self> {
|
pub fn new_from_file() -> Result<Self> {
|
||||||
let mut file = File::open(
|
let mut file = File::open(
|
||||||
Self::path_from_xdg()
|
Self::path_from_xdg()
|
||||||
.or_else(Self::path_from_home)
|
.or_else(|_| Self::path_from_home())
|
||||||
.or_else(Self::path_from_tmp)
|
.or_else(|_| Self::path_from_tmp())
|
||||||
.or_else(|_| Err(Error::GetPathNotFoundError))?,
|
.or_else(|_| Err(Error::GetPathNotFoundError))?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut content = String::new();
|
let mut content = vec![];
|
||||||
file.read_to_string(&mut content)?;
|
file.read_to_end(&mut content)?;
|
||||||
|
|
||||||
Ok(toml::from_str(&content)?)
|
Ok(toml::from_slice(&content)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn email_full(&self) -> String {
|
pub fn email_full(&self) -> String {
|
||||||
format!("{} <{}>", self.name, self.email)
|
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
|
if part
|
||||||
.get_headers()
|
.get_headers()
|
||||||
.get_first_value("content-type")
|
.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()
|
.is_some()
|
||||||
{
|
{
|
||||||
parts.push(part.get_body().unwrap_or(String::new()))
|
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
|
part.subparts
|
||||||
.iter()
|
.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 {
|
pub fn extract_text_bodies(mime: &str, email: &mailparse::ParsedMail) -> String {
|
||||||
let mut parts = vec![];
|
let mut parts = vec![];
|
||||||
extract_text_bodies_into(mime, email, &mut parts);
|
extract_text_bodies_into(&mime, email, &mut parts);
|
||||||
parts.join("\r\n")
|
parts.join("\r\n")
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ pub enum Error {
|
||||||
ParseEmailError(mailparse::MailParseError),
|
ParseEmailError(mailparse::MailParseError),
|
||||||
ReadEmailNotFoundError(String),
|
ReadEmailNotFoundError(String),
|
||||||
ReadEmailEmptyPartError(String, String),
|
ReadEmailEmptyPartError(String, String),
|
||||||
|
ExtractAttachmentsEmptyError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
|
@ -30,6 +31,9 @@ impl fmt::Display for Error {
|
||||||
Error::ReadEmailEmptyPartError(uid, mime) => {
|
Error::ReadEmailEmptyPartError(uid, mime) => {
|
||||||
write!(f, "no {} content found for uid {}", mime, uid)
|
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;
|
mod table;
|
||||||
|
|
||||||
use clap::{App, AppSettings, Arg, SubCommand};
|
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::config::Config;
|
||||||
use crate::imap::ImapConnector;
|
use crate::imap::ImapConnector;
|
||||||
|
@ -76,14 +76,14 @@ fn mailbox_arg() -> Arg<'static, 'static> {
|
||||||
Arg::with_name("mailbox")
|
Arg::with_name("mailbox")
|
||||||
.short("m")
|
.short("m")
|
||||||
.long("mailbox")
|
.long("mailbox")
|
||||||
.help("Name of the targeted mailbox")
|
.help("Name of the mailbox")
|
||||||
.value_name("STRING")
|
.value_name("STRING")
|
||||||
.default_value("INBOX")
|
.default_value("INBOX")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uid_arg() -> Arg<'static, 'static> {
|
fn uid_arg() -> Arg<'static, 'static> {
|
||||||
Arg::with_name("uid")
|
Arg::with_name("uid")
|
||||||
.help("UID of the targeted email")
|
.help("UID of the email")
|
||||||
.value_name("UID")
|
.value_name("UID")
|
||||||
.required(true)
|
.required(true)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ fn run() -> Result<()> {
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("read")
|
SubCommand::with_name("read")
|
||||||
.about("Reads an email by its UID")
|
.about("Reads text bodies of an email")
|
||||||
.arg(uid_arg())
|
.arg(uid_arg())
|
||||||
.arg(mailbox_arg())
|
.arg(mailbox_arg())
|
||||||
.arg(
|
.arg(
|
||||||
|
@ -118,26 +118,32 @@ fn run() -> Result<()> {
|
||||||
.short("t")
|
.short("t")
|
||||||
.long("mime-type")
|
.long("mime-type")
|
||||||
.value_name("STRING")
|
.value_name("STRING")
|
||||||
.possible_values(&["text/plain", "text/html"])
|
.possible_values(&["plain", "html"])
|
||||||
.default_value("text/plain"),
|
.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("write").about("Writes a new email"))
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("reply")
|
SubCommand::with_name("reply")
|
||||||
.about("Replies to an email by its UID")
|
.about("Answers to an email")
|
||||||
.arg(uid_arg())
|
.arg(uid_arg())
|
||||||
.arg(mailbox_arg())
|
.arg(mailbox_arg())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("reply all")
|
Arg::with_name("reply-all")
|
||||||
.help("Replies to all recipients")
|
.help("Including all recipients")
|
||||||
.short("a")
|
.short("a")
|
||||||
.long("all"),
|
.long("all"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("forward")
|
SubCommand::with_name("forward")
|
||||||
.about("Forwards an email by its UID")
|
.about("Forwards an email")
|
||||||
.arg(uid_arg())
|
.arg(uid_arg())
|
||||||
.arg(mailbox_arg()),
|
.arg(mailbox_arg()),
|
||||||
)
|
)
|
||||||
|
@ -190,12 +196,35 @@ fn run() -> Result<()> {
|
||||||
let config = Config::new_from_file()?;
|
let config = Config::new_from_file()?;
|
||||||
let mbox = matches.value_of("mailbox").unwrap();
|
let mbox = matches.value_of("mailbox").unwrap();
|
||||||
let uid = matches.value_of("uid").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)?;
|
let body = ImapConnector::new(&config.imap)?.read_email_body(&mbox, &uid, &mime)?;
|
||||||
|
|
||||||
println!("{}", body);
|
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") {
|
if let Some(_) = matches.subcommand_matches("write") {
|
||||||
let config = Config::new_from_file()?;
|
let config = Config::new_from_file()?;
|
||||||
let mut imap_conn = ImapConnector::new(&config.imap)?;
|
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 = imap_conn.read_msg(&mbox, &uid)?;
|
||||||
let msg = Msg::from(&msg)?;
|
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)?
|
msg.build_reply_all_tpl(&config)?
|
||||||
} else {
|
} else {
|
||||||
msg.build_reply_tpl(&config)?
|
msg.build_reply_tpl(&config)?
|
||||||
|
|
34
src/msg.rs
34
src/msg.rs
|
@ -117,6 +117,40 @@ impl<'a> Msg<'a> {
|
||||||
Ok(msg)
|
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> {
|
pub fn build_new_tpl(config: &Config) -> Result<String> {
|
||||||
let mut tpl = vec![];
|
let mut tpl = vec![];
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue