add text and html previews

This commit is contained in:
Clément DOUIN 2021-01-03 17:28:42 +01:00
parent 187b886a1c
commit 0a48df0567
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
5 changed files with 117 additions and 20 deletions

View file

@ -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
View file

@ -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"

View file

@ -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"] }

View file

@ -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(())
} }

View file

@ -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();
}
}
} }