mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-16 22:33:36 +00:00
complete merge email/msg
This commit is contained in:
parent
0a508f2e95
commit
04642859e8
4 changed files with 114 additions and 302 deletions
229
src/email.rs
229
src/email.rs
|
@ -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")
|
||||
}
|
50
src/imap.rs
50
src/imap.rs
|
@ -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>> {
|
||||
|
|
82
src/main.rs
82
src/main.rs
|
@ -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?")?;
|
||||
|
||||
|
|
55
src/msg.rs
55
src/msg.rs
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue