complete merge email/msg

This commit is contained in:
Clément DOUIN 2021-01-16 15:01:18 +01:00
parent 0a508f2e95
commit 04642859e8
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
4 changed files with 114 additions and 302 deletions

View file

@ -1,229 +0,0 @@
use imap;
use mailparse::{self, MailHeaderMap};
use rfc2047_decoder;
use crate::table::{self, DisplayCell, DisplayRow, DisplayTable};
#[derive(Debug)]
pub struct Uid(pub u32);
impl Uid {
pub fn from_fetch(fetch: &imap::types::Fetch) -> Self {
Self(fetch.uid.unwrap())
}
}
impl DisplayCell for Uid {
fn styles(&self) -> &[table::Style] {
&[table::RED]
}
fn value(&self) -> String {
self.0.to_string()
}
}
#[derive(Debug)]
pub struct Flags<'a>(Vec<imap::types::Flag<'a>>);
impl Flags<'_> {
pub fn from_fetch(fetch: &imap::types::Fetch) -> Self {
let flags = fetch.flags().iter().fold(vec![], |mut flags, flag| {
use imap::types::Flag::*;
match flag {
Seen => flags.push(Seen),
Answered => flags.push(Answered),
Draft => flags.push(Draft),
Flagged => flags.push(Flagged),
_ => (),
};
flags
});
Self(flags)
}
}
impl DisplayCell for Flags<'_> {
fn styles(&self) -> &[table::Style] {
&[table::WHITE]
}
fn value(&self) -> String {
// FIXME
// use imap::types::Flag::*;
// let flags = &self.0;
// let mut flags_str = String::new();
// flags_str.push_str(if flags.contains(&Seen) { &" " } else { &"N" });
// flags_str.push_str(if flags.contains(&Answered) {
// &"R"
// } else {
// &" "
// });
// flags_str.push_str(if flags.contains(&Draft) { &"D" } else { &" " });
// flags_str.push_str(if flags.contains(&Flagged) { &"F" } else { &" " });
// flags_str
String::new()
}
}
#[derive(Debug)]
pub struct Sender(String);
impl Sender {
fn try_from_fetch(fetch: &imap::types::Fetch) -> Option<String> {
let addr = fetch.envelope()?.from.as_ref()?.first()?;
addr.name
.and_then(|bytes| rfc2047_decoder::decode(bytes).ok())
.or_else(|| {
let mbox = String::from_utf8(addr.mailbox?.to_vec()).ok()?;
let host = String::from_utf8(addr.host?.to_vec()).ok()?;
Some(format!("{}@{}", mbox, host))
})
}
pub fn from_fetch(fetch: &imap::types::Fetch) -> Self {
Self(Self::try_from_fetch(fetch).unwrap_or(String::new()))
}
}
impl DisplayCell for Sender {
fn styles(&self) -> &[table::Style] {
&[table::BLUE]
}
fn value(&self) -> String {
self.0.to_owned()
}
}
#[derive(Debug)]
pub struct Subject(String);
impl Subject {
fn try_from_fetch(fetch: &imap::types::Fetch) -> Option<String> {
fetch
.envelope()?
.subject
.and_then(|bytes| rfc2047_decoder::decode(bytes).ok())
.and_then(|subject| Some(subject.replace("\r", "")))
.and_then(|subject| Some(subject.replace("\n", "")))
}
pub fn from_fetch(fetch: &imap::types::Fetch) -> Self {
Self(Self::try_from_fetch(fetch).unwrap_or(String::new()))
}
}
impl DisplayCell for Subject {
fn styles(&self) -> &[table::Style] {
&[table::GREEN]
}
fn value(&self) -> String {
self.0.to_owned()
}
}
#[derive(Debug)]
pub struct Date(String);
impl Date {
fn try_from_fetch(fetch: &imap::types::Fetch) -> Option<String> {
fetch
.internal_date()
.and_then(|date| Some(date.to_rfc3339()))
}
pub fn from_fetch(fetch: &imap::types::Fetch) -> Self {
Self(Self::try_from_fetch(fetch).unwrap_or(String::new()))
}
}
impl DisplayCell for Date {
fn styles(&self) -> &[table::Style] {
&[table::YELLOW]
}
fn value(&self) -> String {
self.0.to_owned()
}
}
#[derive(Debug)]
pub struct Email<'a> {
pub uid: Uid,
pub flags: Flags<'a>,
pub from: Sender,
pub subject: Subject,
pub date: Date,
}
impl Email<'_> {
pub fn from_fetch(fetch: &imap::types::Fetch) -> Self {
Self {
uid: Uid::from_fetch(fetch),
from: Sender::from_fetch(fetch),
subject: Subject::from_fetch(fetch),
date: Date::from_fetch(fetch),
flags: Flags::from_fetch(fetch),
}
}
}
impl<'a> DisplayRow for Email<'a> {
fn to_row(&self) -> Vec<table::Cell> {
vec![
self.uid.to_cell(),
self.flags.to_cell(),
self.from.to_cell(),
self.subject.to_cell(),
self.date.to_cell(),
]
}
}
impl<'a> DisplayTable<'a, Email<'a>> for Vec<Email<'a>> {
fn cols() -> &'a [&'a str] {
&["uid", "flags", "from", "subject", "date"]
}
fn rows(&self) -> &Vec<Email<'a>> {
self
}
}
// Utils
fn extract_text_bodies_into(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(|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);
parts.join("\r\n")
}

View file

@ -3,7 +3,6 @@ use native_tls::{self, TlsConnector, TlsStream};
use std::{fmt, net::TcpStream, result};
use crate::config;
use crate::email::{self, Email};
use crate::mbox::Mbox;
use crate::msg::Msg;
@ -114,49 +113,34 @@ impl<'a> ImapConnector<'a> {
Ok(msgs)
}
pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result<Vec<Email<'_>>> {
pub fn search_msgs(
&mut self,
mbox: &str,
query: &str,
page_size: &usize,
page: &usize,
) -> Result<Vec<Msg>> {
self.sess.select(mbox)?;
let begin = page * page_size;
let end = begin + (page_size - 1);
let uids = self
.sess
.uid_search(query)?
.search(query)?
.iter()
.map(|n| n.to_string())
.map(|seq| seq.to_string())
.collect::<Vec<_>>();
let range = uids[begin..end.min(uids.len())].join(",");
let emails = self
let msgs = self
.sess
.uid_fetch(
uids[..20.min(uids.len())].join(","),
"(UID ENVELOPE INTERNALDATE)",
)?
.fetch(range, "(UID ENVELOPE INTERNALDATE)")?
.iter()
.map(Email::from_fetch)
.rev()
.map(Msg::from)
.collect::<Vec<_>>();
Ok(emails)
}
pub fn read_email_body(&mut self, mbox: &str, uid: &str, mime: &str) -> Result<String> {
self.sess.select(mbox)?;
match self.sess.uid_fetch(uid, "BODY[]")?.first() {
None => Err(Error::ReadEmailNotFoundError(uid.to_string())),
Some(fetch) => {
let bytes = fetch.body().unwrap_or(&[]);
let email = mailparse::parse_mail(bytes)?;
let bodies = email::extract_text_bodies(&mime, &email);
if bodies.is_empty() {
Err(Error::ReadEmailEmptyPartError(
uid.to_string(),
mime.to_string(),
))
} else {
Ok(bodies)
}
}
}
Ok(msgs)
}
pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result<Vec<u8>> {

View file

@ -1,5 +1,4 @@
mod config;
mod email;
mod imap;
mod input;
mod mbox;
@ -15,8 +14,8 @@ use crate::imap::ImapConnector;
use crate::msg::Msg;
use crate::table::DisplayTable;
const DEFAULT_PAGE_SIZE: u32 = 10;
const DEFAULT_PAGE: u32 = 0;
const DEFAULT_PAGE_SIZE: usize = 10;
const DEFAULT_PAGE: usize = 0;
#[derive(Debug)]
pub enum Error {
@ -91,9 +90,27 @@ fn uid_arg() -> Arg<'static, 'static> {
.required(true)
}
fn page_size_arg<'a>(default: &'a str) -> Arg<'a, 'a> {
Arg::with_name("size")
.help("Page size")
.short("s")
.long("size")
.value_name("INT")
.default_value(default)
}
fn page_arg<'a>(default: &'a str) -> Arg<'a, 'a> {
Arg::with_name("page")
.help("Page number")
.short("p")
.long("page")
.value_name("INT")
.default_value(default)
}
fn run() -> Result<()> {
let default_page_size = &DEFAULT_PAGE_SIZE.to_string();
let default_page = &DEFAULT_PAGE.to_string();
let default_page_size_str = &DEFAULT_PAGE_SIZE.to_string();
let default_page_str = &DEFAULT_PAGE.to_string();
let matches = App::new("Himalaya")
.version("0.1.0")
@ -110,28 +127,16 @@ fn run() -> Result<()> {
.aliases(&["lst", "l"])
.about("Lists emails sorted by arrival date")
.arg(mailbox_arg())
.arg(
Arg::with_name("size")
.help("Page size")
.short("s")
.long("size")
.value_name("INT")
.default_value(default_page_size),
)
.arg(
Arg::with_name("page")
.help("Page number")
.short("p")
.long("page")
.value_name("INT")
.default_value(default_page),
),
.arg(page_size_arg(default_page_size_str))
.arg(page_arg(default_page_str)),
)
.subcommand(
SubCommand::with_name("search")
.aliases(&["query", "q", "s"])
.about("Lists emails matching the given IMAP query")
.arg(mailbox_arg())
.arg(page_size_arg(default_page_size_str))
.arg(page_arg(default_page_str))
.arg(
Arg::with_name("query")
.help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)")
@ -205,12 +210,12 @@ fn run() -> Result<()> {
.value_of("size")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE_SIZE);
.unwrap_or(DEFAULT_PAGE_SIZE as u32);
let page: u32 = matches
.value_of("page")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE);
.unwrap_or(DEFAULT_PAGE as u32);
let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?;
println!("{}", msgs.to_table());
@ -223,6 +228,16 @@ fn run() -> Result<()> {
let mut imap_conn = ImapConnector::new(&config.imap)?;
let mbox = matches.value_of("mailbox").unwrap();
let page_size: usize = matches
.value_of("size")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE_SIZE);
let page: usize = matches
.value_of("page")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE);
let query = matches
.values_of("query")
.unwrap_or_default()
@ -248,7 +263,7 @@ fn run() -> Result<()> {
.1
.join(" ");
let msgs = imap_conn.read_emails(&mbox, &query)?;
let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?;
println!("{}", msgs.to_table());
imap_conn.close();
@ -262,8 +277,9 @@ fn run() -> Result<()> {
let uid = matches.value_of("uid").unwrap();
let mime = format!("text/{}", matches.value_of("mime-type").unwrap());
let body = imap_conn.read_email_body(&mbox, &uid, &mime)?;
println!("{}", body);
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let text_bodies = msg.text_bodies(&mime)?;
println!("{}", text_bodies);
imap_conn.close();
}
@ -275,8 +291,8 @@ fn run() -> Result<()> {
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let parts = msg.extract_parts()?;
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let parts = msg.extract_attachments()?;
if parts.is_empty() {
println!("No attachment found for message {}", uid);
@ -299,7 +315,7 @@ fn run() -> Result<()> {
let tpl = Msg::build_new_tpl(&config)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes());
let msg = Msg::from(content);
input::ask_for_confirmation("Send the message?")?;
@ -318,7 +334,7 @@ fn run() -> Result<()> {
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let tpl = if matches.is_present("reply-all") {
msg.build_reply_all_tpl(&config)?
} else {
@ -326,7 +342,7 @@ fn run() -> Result<()> {
};
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes());
let msg = Msg::from(content);
input::ask_for_confirmation("Send the message?")?;
@ -345,10 +361,10 @@ fn run() -> Result<()> {
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let tpl = msg.build_forward_tpl(&config)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes());
let msg = Msg::from(content);
input::ask_for_confirmation("Send the message?")?;

View file

@ -48,12 +48,22 @@ pub struct Msg {
raw: Vec<u8>,
}
impl From<&[u8]> for Msg {
fn from(item: &[u8]) -> Self {
impl From<String> for Msg {
fn from(item: String) -> Self {
Self {
uid: 0,
flags: vec![],
raw: item.to_vec(),
raw: item.as_bytes().to_vec(),
}
}
}
impl From<Vec<u8>> for Msg {
fn from(item: Vec<u8>) -> Self {
Self {
uid: 0,
flags: vec![],
raw: item,
}
}
}
@ -133,7 +143,38 @@ impl<'a> Msg {
Ok(msg)
}
fn extract_parts_into(part: &mailparse::ParsedMail, parts: &mut Vec<(String, Vec<u8>)>) {
fn extract_text_bodies_into(part: &mailparse::ParsedMail, mime: &str, parts: &mut Vec<String>) {
match part.subparts.len() {
0 => {
let content_type = part
.get_headers()
.get_first_value("content-type")
.unwrap_or_default();
if content_type.starts_with(mime) {
parts.push(part.get_body().unwrap_or_default())
}
}
_ => {
part.subparts
.iter()
.for_each(|part| Self::extract_text_bodies_into(part, mime, parts));
}
}
}
fn extract_text_bodies(&self, mime: &str) -> Result<Vec<String>> {
let mut parts = vec![];
Self::extract_text_bodies_into(&self.parse()?, mime, &mut parts);
Ok(parts)
}
pub fn text_bodies(&self, mime: &str) -> Result<String> {
let text_bodies = self.extract_text_bodies(mime)?;
Ok(text_bodies.join("\r\n"))
}
fn extract_attachments_into(part: &mailparse::ParsedMail, parts: &mut Vec<(String, Vec<u8>)>) {
match part.subparts.len() {
0 => {
let content_disp = part.get_content_disposition();
@ -156,14 +197,14 @@ impl<'a> Msg {
_ => {
part.subparts
.iter()
.for_each(|part| Self::extract_parts_into(part, parts));
.for_each(|part| Self::extract_attachments_into(part, parts));
}
}
}
pub fn extract_parts(&self) -> Result<Vec<(String, Vec<u8>)>> {
pub fn extract_attachments(&self) -> Result<Vec<(String, Vec<u8>)>> {
let mut parts = vec![];
Self::extract_parts_into(&self.parse()?, &mut parts);
Self::extract_attachments_into(&self.parse()?, &mut parts);
Ok(parts)
}