mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-25 04:20:22 +00:00
add text and html previews
This commit is contained in:
parent
187b886a1c
commit
0a48df0567
5 changed files with 117 additions and 20 deletions
|
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- List new emails [#6]
|
- List new emails [#6]
|
||||||
- Set up CLI arg parser [#15]
|
- Set up CLI arg parser [#15]
|
||||||
- List mailboxes command [#5]
|
- List mailboxes command [#5]
|
||||||
|
- Text and HTML previews [#12] [#13]
|
||||||
|
|
||||||
[unreleased]: https://github.com/soywod/himalaya
|
[unreleased]: https://github.com/soywod/himalaya
|
||||||
|
|
||||||
|
@ -22,4 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
[#2]: https://github.com/soywod/himalaya/issues/2
|
[#2]: https://github.com/soywod/himalaya/issues/2
|
||||||
[#3]: https://github.com/soywod/himalaya/issues/3
|
[#3]: https://github.com/soywod/himalaya/issues/3
|
||||||
[#5]: https://github.com/soywod/himalaya/issues/5
|
[#5]: https://github.com/soywod/himalaya/issues/5
|
||||||
|
[#12]: https://github.com/soywod/himalaya/issues/12
|
||||||
|
[#13]: https://github.com/soywod/himalaya/issues/13
|
||||||
[#15]: https://github.com/soywod/himalaya/issues/15
|
[#15]: https://github.com/soywod/himalaya/issues/15
|
||||||
|
|
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -50,6 +50,12 @@ dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
|
@ -196,6 +202,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"imap",
|
"imap",
|
||||||
|
"mailparse",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"rfc2047-decoder",
|
"rfc2047-decoder",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -261,6 +268,17 @@ dependencies = [
|
||||||
"cfg-if 0.1.10",
|
"cfg-if 0.1.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mailparse"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "388a77a7f161b32d0314404306b8ed5966b34b797fc9ef6bcf6686935162da3c"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.12.3",
|
||||||
|
"charset",
|
||||||
|
"quoted_printable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.3.4"
|
version = "2.3.4"
|
||||||
|
|
|
@ -8,6 +8,7 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.33.3"
|
clap = "2.33.3"
|
||||||
imap = "2.4.0"
|
imap = "2.4.0"
|
||||||
|
mailparse = "0.13.1"
|
||||||
native-tls = "0.2"
|
native-tls = "0.2"
|
||||||
rfc2047-decoder = "0.1.2"
|
rfc2047-decoder = "0.1.2"
|
||||||
serde = { version = "1.0.118", features = ["derive"] }
|
serde = { version = "1.0.118", features = ["derive"] }
|
||||||
|
|
67
src/imap.rs
67
src/imap.rs
|
@ -1,4 +1,5 @@
|
||||||
use imap;
|
use imap;
|
||||||
|
use mailparse::{self, MailHeaderMap};
|
||||||
use native_tls::{TlsConnector, TlsStream};
|
use native_tls::{TlsConnector, TlsStream};
|
||||||
use rfc2047_decoder;
|
use rfc2047_decoder;
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
|
@ -99,13 +100,17 @@ fn date_from_fetch(fetch: &imap::types::Fetch) -> String {
|
||||||
pub fn read_emails(imap_sess: &mut ImapSession, mbox: &str, query: &str) -> imap::Result<()> {
|
pub fn read_emails(imap_sess: &mut ImapSession, mbox: &str, query: &str) -> imap::Result<()> {
|
||||||
imap_sess.select(mbox)?;
|
imap_sess.select(mbox)?;
|
||||||
|
|
||||||
let seqs = imap_sess
|
let uids = imap_sess
|
||||||
.search(query)?
|
.uid_search(query)?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|n| n.to_string())
|
.map(|n| n.to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let table_head = vec![
|
let table_head = vec![
|
||||||
|
table::Cell::new(
|
||||||
|
vec![table::BOLD, table::UNDERLINE, table::WHITE],
|
||||||
|
String::from("ID"),
|
||||||
|
),
|
||||||
table::Cell::new(
|
table::Cell::new(
|
||||||
vec![table::BOLD, table::UNDERLINE, table::WHITE],
|
vec![table::BOLD, table::UNDERLINE, table::WHITE],
|
||||||
String::from("FLAGS"),
|
String::from("FLAGS"),
|
||||||
|
@ -125,13 +130,14 @@ pub fn read_emails(imap_sess: &mut ImapSession, mbox: &str, query: &str) -> imap
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut table_rows = imap_sess
|
let mut table_rows = imap_sess
|
||||||
.fetch(
|
.uid_fetch(
|
||||||
seqs[..20.min(seqs.len())].join(","),
|
uids[..20.min(uids.len())].join(","),
|
||||||
"(INTERNALDATE ENVELOPE)",
|
"(INTERNALDATE ENVELOPE UID)",
|
||||||
)?
|
)?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|fetch| {
|
.map(|fetch| {
|
||||||
vec![
|
vec![
|
||||||
|
table::Cell::new(vec![table::RED], fetch.uid.unwrap_or(0).to_string()),
|
||||||
table::Cell::new(vec![table::WHITE], String::from("!@")),
|
table::Cell::new(vec![table::WHITE], String::from("!@")),
|
||||||
table::Cell::new(vec![table::BLUE], first_addr_from_fetch(fetch)),
|
table::Cell::new(vec![table::BLUE], first_addr_from_fetch(fetch)),
|
||||||
table::Cell::new(vec![table::GREEN], subject_from_fetch(fetch)),
|
table::Cell::new(vec![table::GREEN], subject_from_fetch(fetch)),
|
||||||
|
@ -186,9 +192,58 @@ pub fn list_mailboxes(imap_sess: &mut ImapSession) -> imap::Result<()> {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if table_rows.len() == 0 {
|
||||||
|
println!("No email found!");
|
||||||
|
} else {
|
||||||
table_rows.insert(0, table_head);
|
table_rows.insert(0, table_head);
|
||||||
|
|
||||||
println!("{}", table::render(table_rows));
|
println!("{}", table::render(table_rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_subparts_by_mime(mime: &str, part: &mailparse::ParsedMail, parts: &mut Vec<String>) {
|
||||||
|
match part.subparts.len() {
|
||||||
|
0 => {
|
||||||
|
if part
|
||||||
|
.get_headers()
|
||||||
|
.get_first_value("content-type")
|
||||||
|
.and_then(|v| if v.starts_with(mime) { Some(()) } else { None })
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
parts.push(part.get_body().unwrap_or(String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
part.subparts
|
||||||
|
.iter()
|
||||||
|
.for_each(|p| extract_subparts_by_mime(mime, p, parts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_email(
|
||||||
|
imap_sess: &mut ImapSession,
|
||||||
|
mbox: &str,
|
||||||
|
uid: &str,
|
||||||
|
mime: &str,
|
||||||
|
) -> imap::Result<()> {
|
||||||
|
imap_sess.select(mbox)?;
|
||||||
|
|
||||||
|
match imap_sess.uid_fetch(uid, "BODY[]")?.first() {
|
||||||
|
None => println!("No email found in mailbox {} with UID {}", mbox, uid),
|
||||||
|
Some(email_raw) => {
|
||||||
|
let email = mailparse::parse_mail(email_raw.body().unwrap_or(&[])).unwrap();
|
||||||
|
let mut parts = vec![];
|
||||||
|
extract_subparts_by_mime(mime, &email, &mut parts);
|
||||||
|
|
||||||
|
if parts.len() == 0 {
|
||||||
|
println!("No {} content found for email {}!", mime, uid);
|
||||||
|
} else {
|
||||||
|
println!("{}", parts.join("\r\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
44
src/main.rs
44
src/main.rs
|
@ -6,9 +6,11 @@ use clap::{App, Arg, SubCommand};
|
||||||
|
|
||||||
fn mailbox_arg() -> Arg<'static, 'static> {
|
fn mailbox_arg() -> Arg<'static, 'static> {
|
||||||
Arg::with_name("mailbox")
|
Arg::with_name("mailbox")
|
||||||
|
.short("m")
|
||||||
|
.long("mailbox")
|
||||||
.help("Name of the targeted mailbox")
|
.help("Name of the targeted mailbox")
|
||||||
.value_name("MAILBOX")
|
.value_name("STRING")
|
||||||
.required(true)
|
.default_value("INBOX")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uid_arg() -> Arg<'static, 'static> {
|
fn uid_arg() -> Arg<'static, 'static> {
|
||||||
|
@ -26,37 +28,46 @@ fn main() {
|
||||||
.version("0.1.0")
|
.version("0.1.0")
|
||||||
.about("📫 Minimalist CLI email client")
|
.about("📫 Minimalist CLI email client")
|
||||||
.author("soywod <clement.douin@posteo.net>")
|
.author("soywod <clement.douin@posteo.net>")
|
||||||
|
.subcommand(SubCommand::with_name("list").about("Lists all available mailboxes"))
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("query")
|
SubCommand::with_name("search")
|
||||||
.about("Prints emails filtered by the given IMAP query")
|
.about("Lists emails matching the given IMAP query")
|
||||||
.arg(mailbox_arg())
|
.arg(mailbox_arg())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("query")
|
Arg::with_name("query")
|
||||||
.help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)")
|
.help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)")
|
||||||
.value_name("COMMANDS")
|
.value_name("QUERY")
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.required(true),
|
.required(true),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.subcommand(SubCommand::with_name("list").about("Lists all available mailboxes"))
|
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("read")
|
SubCommand::with_name("read")
|
||||||
.about("Reads an email by its UID")
|
.about("Reads an email by its UID")
|
||||||
|
.arg(uid_arg())
|
||||||
.arg(mailbox_arg())
|
.arg(mailbox_arg())
|
||||||
.arg(uid_arg()),
|
.arg(
|
||||||
|
Arg::with_name("mime-type")
|
||||||
|
.help("MIME type to use")
|
||||||
|
.short("t")
|
||||||
|
.long("mime-type")
|
||||||
|
.value_name("STRING")
|
||||||
|
.possible_values(&["text/plain", "text/html"])
|
||||||
|
.default_value("text/plain"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.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("forward")
|
SubCommand::with_name("forward")
|
||||||
.about("Forwards an email by its UID")
|
.about("Forwards an email by its UID")
|
||||||
.arg(mailbox_arg())
|
.arg(uid_arg())
|
||||||
.arg(uid_arg()),
|
.arg(mailbox_arg()),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("reply")
|
SubCommand::with_name("reply")
|
||||||
.about("Replies to an email by its UID")
|
.about("Replies to an email by its UID")
|
||||||
.arg(mailbox_arg())
|
|
||||||
.arg(uid_arg())
|
.arg(uid_arg())
|
||||||
|
.arg(mailbox_arg())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("reply all")
|
Arg::with_name("reply all")
|
||||||
.help("Replies to all recipients")
|
.help("Replies to all recipients")
|
||||||
|
@ -66,8 +77,8 @@ fn main() {
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("query") {
|
if let Some(matches) = matches.subcommand_matches("search") {
|
||||||
let mbox = matches.value_of("mailbox").unwrap_or("inbox");
|
let mbox = matches.value_of("mailbox").unwrap();
|
||||||
|
|
||||||
if let Some(matches) = matches.values_of("query") {
|
if let Some(matches) = matches.values_of("query") {
|
||||||
let query = matches
|
let query = matches
|
||||||
|
@ -100,4 +111,13 @@ fn main() {
|
||||||
if let Some(_) = matches.subcommand_matches("list") {
|
if let Some(_) = matches.subcommand_matches("list") {
|
||||||
imap::list_mailboxes(&mut imap_sess).unwrap();
|
imap::list_mailboxes(&mut imap_sess).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(matches) = matches.subcommand_matches("read") {
|
||||||
|
let mbox = matches.value_of("mailbox").unwrap();
|
||||||
|
let mime = matches.value_of("mime-type").unwrap();
|
||||||
|
|
||||||
|
if let Some(uid) = matches.value_of("uid") {
|
||||||
|
imap::read_email(&mut imap_sess, mbox, uid, mime).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue