diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f974cc9..50fea80 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -20,7 +20,7 @@ section = "mail" imap-backend = ["imap", "imap-proto"] maildir-backend = ["maildir", "md5"] notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend", "notmuch-backend"] +default = ["imap-backend", "maildir-backend"] [dependencies] ammonia = "3.1.2" diff --git a/cli/src/backends/backend.rs b/cli/src/backends/backend.rs index 92dc363..0162d05 100644 --- a/cli/src/backends/backend.rs +++ b/cli/src/backends/backend.rs @@ -4,9 +4,9 @@ //! custom backend implementations. use anyhow::Result; -use himalaya_lib::mbox::Mboxes; +use himalaya_lib::{mbox::Mboxes, msg::Envelopes}; -use crate::msg::{Envelopes, Msg}; +use crate::msg::Msg; pub trait Backend<'a> { fn connect(&mut self) -> Result<()> { @@ -16,12 +16,7 @@ pub trait Backend<'a> { fn add_mbox(&mut self, mbox: &str) -> Result<()>; fn get_mboxes(&mut self) -> Result; fn del_mbox(&mut self, mbox: &str) -> Result<()>; - fn get_envelopes( - &mut self, - mbox: &str, - page_size: usize, - page: usize, - ) -> Result>; + fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result; fn search_envelopes( &mut self, mbox: &str, @@ -29,7 +24,7 @@ pub trait Backend<'a> { sort: &str, page_size: usize, page: usize, - ) -> Result>; + ) -> Result; fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result; fn get_msg(&mut self, mbox: &str, id: &str) -> Result; fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>; diff --git a/cli/src/backends/imap/imap_backend.rs b/cli/src/backends/imap/imap_backend.rs index 76ed158..71e9a32 100644 --- a/cli/src/backends/imap/imap_backend.rs +++ b/cli/src/backends/imap/imap_backend.rs @@ -6,25 +6,22 @@ use anyhow::{anyhow, Context, Result}; use himalaya_lib::{ account::{AccountConfig, ImapBackendConfig}, mbox::{Mbox, Mboxes}, + msg::{Envelopes, Flags}, }; use imap::types::NameAttribute; use log::{debug, log_enabled, trace, Level}; use native_tls::{TlsConnector, TlsStream}; -use std::{ - collections::HashSet, - convert::{TryFrom, TryInto}, - net::TcpStream, - thread, -}; +use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread}; use crate::{ - backends::{imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes}, - msg::{Envelopes, Msg}, + backends::{ + from_imap_fetch, from_imap_fetches, imap::msg_sort_criterion::SortCriteria, + into_imap_flags, Backend, + }, + msg::Msg, output::run_cmd, }; -use super::ImapFlags; - type ImapSess = imap::Session>; pub struct ImapBackend<'a> { @@ -148,7 +145,7 @@ impl<'a> ImapBackend<'a> { .context("cannot fetch new messages enveloppe")?; for fetch in fetches.iter() { - let msg = ImapEnvelope::try_from(fetch)?; + let msg = from_imap_fetch(fetch)?; let uid = fetch.uid.ok_or_else(|| { anyhow!("cannot retrieve message {}'s UID", fetch.message) })?; @@ -252,12 +249,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .context(format!("cannot delete imap mailbox {:?}", mbox)) } - fn get_envelopes( - &mut self, - mbox: &str, - page_size: usize, - page: usize, - ) -> Result> { + fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result { let last_seq = self .sess()? .select(mbox) @@ -265,7 +257,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .exists as usize; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let range = if page_size > 0 { @@ -282,8 +274,8 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .context(format!("cannot fetch messages within range {:?}", range))?; - let envelopes: ImapEnvelopes = fetches.try_into()?; - Ok(Box::new(envelopes)) + + from_imap_fetches(fetches) } fn search_envelopes( @@ -293,7 +285,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { sort: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { let last_seq = self .sess()? .select(mbox) @@ -301,7 +293,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .exists; debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let begin = page * page_size; @@ -330,7 +322,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .collect() }; if seqs.is_empty() { - return Ok(Box::new(ImapEnvelopes::default())); + return Ok(Envelopes::default()); } let range = seqs[begin..end.min(seqs.len())].join(","); @@ -338,15 +330,15 @@ impl<'a> Backend<'a> for ImapBackend<'a> { .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .context(format!("cannot fetch messages within range {:?}", range))?; - let envelopes: ImapEnvelopes = fetches.try_into()?; - Ok(Box::new(envelopes)) + + from_imap_fetches(fetches) } fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .append(mbox, msg) - .flags(>>>::into(flags)) + .flags(into_imap_flags(&flags)) .finish() .context(format!("cannot append message to {:?}", mbox))?; let last_seq = self @@ -396,7 +388,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))?; @@ -410,7 +402,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))?; @@ -421,7 +413,7 @@ impl<'a> Backend<'a> for ImapBackend<'a> { } fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: ImapFlags = flags.into(); + let flags: Flags = flags.into(); self.sess()? .select(mbox) .context(format!("cannot select mailbox {:?}", mbox))?; diff --git a/cli/src/backends/imap/imap_envelope.rs b/cli/src/backends/imap/imap_envelope.rs index d087eb1..8a90e3e 100644 --- a/cli/src/backends/imap/imap_envelope.rs +++ b/cli/src/backends/imap/imap_envelope.rs @@ -3,185 +3,79 @@ //! This module provides IMAP types and conversion utilities related //! to the envelope. -use anyhow::{anyhow, Context, Error, Result}; -use std::{convert::TryFrom, ops::Deref}; +use anyhow::{anyhow, Context, Result}; +use himalaya_lib::msg::Envelope; -use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -use super::{ImapFlag, ImapFlags}; - -/// Represents a list of IMAP envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct ImapEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for ImapEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl PrintTable for ImapEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -// impl Envelopes for ImapEnvelopes { -// // -// } - -/// Represents the IMAP envelope. The envelope is just a message -/// subset, and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct ImapEnvelope { - /// Represents the sequence number of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2 - pub id: u32, - - /// Represents the flags attached to the message. - pub flags: ImapFlags, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the internal date of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3 - pub date: Option, -} - -impl Table for ImapEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("ID").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let id = self.id.to_string(); - let flags = self.flags.to_symbols_string(); - let unseen = !self.flags.contains(&ImapFlag::Seen); - let subject = &self.subject; - let sender = &self.sender; - let date = self.date.as_deref().unwrap_or_default(); - Row::new() - .cell(Cell::new(id).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopes returned by the `imap` crate. -pub type RawImapEnvelopes = imap::types::ZeroCopy>; - -impl TryFrom for ImapEnvelopes { - type Error = Error; - - fn try_from(raw_envelopes: RawImapEnvelopes) -> Result { - let mut envelopes = vec![]; - for raw_envelope in raw_envelopes.iter().rev() { - envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?); - } - Ok(Self { envelopes }) - } -} +use super::from_imap_flags; /// Represents the raw envelope returned by the `imap` crate. -pub type RawImapEnvelope = imap::types::Fetch; +pub type ImapFetch = imap::types::Fetch; -impl TryFrom<&RawImapEnvelope> for ImapEnvelope { - type Error = Error; +pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { + let envelope = fetch + .envelope() + .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?; - fn try_from(fetch: &RawImapEnvelope) -> Result { - let envelope = fetch - .envelope() - .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?; + let id = fetch.message.to_string(); - // Get the sequence number - let id = fetch.message; + let flags = from_imap_flags(fetch.flags()); - // Get the flags - let flags = ImapFlags::try_from(fetch.flags())?; - - // Get the subject - let subject = envelope - .subject - .as_ref() - .map(|subj| { - rfc2047_decoder::decode(subj).context(format!( - "cannot decode subject of message {}", - fetch.message - )) - }) - .unwrap_or_else(|| Ok(String::default()))?; - - // Get the sender - let sender = envelope - .sender - .as_ref() - .and_then(|addrs| addrs.get(0)) - .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) - .ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?; - let sender = if let Some(ref name) = sender.name { - rfc2047_decoder::decode(&name.to_vec()).context(format!( - "cannot decode sender's name of message {}", - fetch.message, - ))? - } else { - let mbox = sender - .mailbox - .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) - .and_then(|mbox| { - rfc2047_decoder::decode(&mbox.to_vec()).context(format!( - "cannot decode sender's mailbox of message {}", - fetch.message, - )) - })?; - let host = sender - .host - .as_ref() - .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) - .and_then(|host| { - rfc2047_decoder::decode(&host.to_vec()).context(format!( - "cannot decode sender's host of message {}", - fetch.message, - )) - })?; - format!("{}@{}", mbox, host) - }; - - // Get the internal date - let date = fetch - .internal_date() - .map(|date| date.naive_local().to_string()); - - Ok(Self { - id, - flags, - subject, - sender, - date, + let subject = envelope + .subject + .as_ref() + .map(|subj| { + rfc2047_decoder::decode(subj).context(format!( + "cannot decode subject of message {}", + fetch.message + )) }) - } + .unwrap_or_else(|| Ok(String::default()))?; + + let sender = envelope + .sender + .as_ref() + .and_then(|addrs| addrs.get(0)) + .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) + .ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?; + let sender = if let Some(ref name) = sender.name { + rfc2047_decoder::decode(&name.to_vec()).context(format!( + "cannot decode sender's name of message {}", + fetch.message, + ))? + } else { + let mbox = sender + .mailbox + .as_ref() + .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) + .and_then(|mbox| { + rfc2047_decoder::decode(&mbox.to_vec()).context(format!( + "cannot decode sender's mailbox of message {}", + fetch.message, + )) + })?; + let host = sender + .host + .as_ref() + .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) + .and_then(|host| { + rfc2047_decoder::decode(&host.to_vec()).context(format!( + "cannot decode sender's host of message {}", + fetch.message, + )) + })?; + format!("{}@{}", mbox, host) + }; + + let date = fetch + .internal_date() + .map(|date| date.naive_local().to_string()); + + Ok(Envelope { + id: id.clone(), + internal_id: id, + flags, + subject, + sender, + date, + }) } diff --git a/cli/src/backends/imap/imap_envelopes.rs b/cli/src/backends/imap/imap_envelopes.rs new file mode 100644 index 0000000..d0e6591 --- /dev/null +++ b/cli/src/backends/imap/imap_envelopes.rs @@ -0,0 +1,15 @@ +use anyhow::{Context, Result}; +use himalaya_lib::msg::Envelopes; + +use crate::backends::{imap::from_imap_fetch, ImapFetch}; + +/// Represents the list of raw envelopes returned by the `imap` crate. +pub type ImapFetches = imap::types::ZeroCopy>; + +pub fn from_imap_fetches(fetches: ImapFetches) -> Result { + let mut envelopes = Envelopes::default(); + for fetch in fetches.iter().rev() { + envelopes.push(from_imap_fetch(fetch).context("cannot parse imap fetch")?); + } + Ok(envelopes) +} diff --git a/cli/src/backends/imap/imap_flag.rs b/cli/src/backends/imap/imap_flag.rs index 40fdb67..1a24ab0 100644 --- a/cli/src/backends/imap/imap_flag.rs +++ b/cli/src/backends/imap/imap_flag.rs @@ -1,151 +1,15 @@ -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - fmt, - ops::Deref, -}; +use himalaya_lib::msg::Flag; -/// Represents the imap flag variants. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub enum ImapFlag { - Seen, - Answered, - Flagged, - Deleted, - Draft, - Recent, - MayCreate, - Custom(String), -} - -impl From<&str> for ImapFlag { - fn from(flag_str: &str) -> Self { - match flag_str { - "seen" => ImapFlag::Seen, - "answered" | "replied" => ImapFlag::Answered, - "flagged" => ImapFlag::Flagged, - "deleted" | "trashed" => ImapFlag::Deleted, - "draft" => ImapFlag::Draft, - "recent" => ImapFlag::Recent, - "maycreate" | "may-create" => ImapFlag::MayCreate, - flag_str => ImapFlag::Custom(flag_str.into()), - } - } -} - -impl TryFrom<&imap::types::Flag<'_>> for ImapFlag { - type Error = Error; - - fn try_from(flag: &imap::types::Flag<'_>) -> Result { - Ok(match flag { - imap::types::Flag::Seen => ImapFlag::Seen, - imap::types::Flag::Answered => ImapFlag::Answered, - imap::types::Flag::Flagged => ImapFlag::Flagged, - imap::types::Flag::Deleted => ImapFlag::Deleted, - imap::types::Flag::Draft => ImapFlag::Draft, - imap::types::Flag::Recent => ImapFlag::Recent, - imap::types::Flag::MayCreate => ImapFlag::MayCreate, - imap::types::Flag::Custom(custom) => ImapFlag::Custom(custom.to_string()), - _ => return Err(anyhow!("cannot parse imap flag")), - }) - } -} - -/// Represents the imap flags. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)] -pub struct ImapFlags(pub Vec); - -impl ImapFlags { - /// Builds a symbols string - pub fn to_symbols_string(&self) -> String { - let mut flags = String::new(); - flags.push_str(if self.contains(&ImapFlag::Seen) { - " " - } else { - "✷" - }); - flags.push_str(if self.contains(&ImapFlag::Answered) { - "↵" - } else { - " " - }); - flags.push_str(if self.contains(&ImapFlag::Flagged) { - "⚑" - } else { - " " - }); - flags - } -} - -impl Deref for ImapFlags { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl fmt::Display for ImapFlags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut glue = ""; - - for flag in &self.0 { - write!(f, "{}", glue)?; - match flag { - ImapFlag::Seen => write!(f, "\\Seen")?, - ImapFlag::Answered => write!(f, "\\Answered")?, - ImapFlag::Flagged => write!(f, "\\Flagged")?, - ImapFlag::Deleted => write!(f, "\\Deleted")?, - ImapFlag::Draft => write!(f, "\\Draft")?, - ImapFlag::Recent => write!(f, "\\Recent")?, - ImapFlag::MayCreate => write!(f, "\\MayCreate")?, - ImapFlag::Custom(custom) => write!(f, "{}", custom)?, - } - glue = " "; - } - - Ok(()) - } -} - -impl<'a> Into>> for ImapFlags { - fn into(self) -> Vec> { - self.0 - .into_iter() - .map(|flag| match flag { - ImapFlag::Seen => imap::types::Flag::Seen, - ImapFlag::Answered => imap::types::Flag::Answered, - ImapFlag::Flagged => imap::types::Flag::Flagged, - ImapFlag::Deleted => imap::types::Flag::Deleted, - ImapFlag::Draft => imap::types::Flag::Draft, - ImapFlag::Recent => imap::types::Flag::Recent, - ImapFlag::MayCreate => imap::types::Flag::MayCreate, - ImapFlag::Custom(custom) => imap::types::Flag::Custom(custom.into()), - }) - .collect() - } -} - -impl From<&str> for ImapFlags { - fn from(flags_str: &str) -> Self { - ImapFlags( - flags_str - .split_whitespace() - .map(|flag_str| flag_str.trim().into()) - .collect(), - ) - } -} - -impl TryFrom<&[imap::types::Flag<'_>]> for ImapFlags { - type Error = Error; - - fn try_from(flags: &[imap::types::Flag<'_>]) -> Result { - let mut f = vec![]; - for flag in flags { - f.push(flag.try_into()?); - } - Ok(Self(f)) +pub fn from_imap_flag(imap_flag: &imap::types::Flag<'_>) -> Flag { + match imap_flag { + imap::types::Flag::Seen => Flag::Seen, + imap::types::Flag::Answered => Flag::Answered, + imap::types::Flag::Flagged => Flag::Flagged, + imap::types::Flag::Deleted => Flag::Deleted, + imap::types::Flag::Draft => Flag::Draft, + imap::types::Flag::Recent => Flag::Recent, + imap::types::Flag::MayCreate => Flag::Custom(String::from("MayCreate")), + imap::types::Flag::Custom(flag) => Flag::Custom(flag.to_string()), + flag => Flag::Custom(flag.to_string()), } } diff --git a/cli/src/backends/imap/imap_flags.rs b/cli/src/backends/imap/imap_flags.rs new file mode 100644 index 0000000..186328a --- /dev/null +++ b/cli/src/backends/imap/imap_flags.rs @@ -0,0 +1,22 @@ +use himalaya_lib::msg::{Flag, Flags}; + +use super::from_imap_flag; + +pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec> { + flags + .iter() + .map(|flag| match flag { + Flag::Seen => imap::types::Flag::Seen, + Flag::Answered => imap::types::Flag::Answered, + Flag::Flagged => imap::types::Flag::Flagged, + Flag::Deleted => imap::types::Flag::Deleted, + Flag::Draft => imap::types::Flag::Draft, + Flag::Recent => imap::types::Flag::Recent, + Flag::Custom(flag) => imap::types::Flag::Custom(flag.into()), + }) + .collect() +} + +pub fn from_imap_flags(imap_flags: &[imap::types::Flag<'_>]) -> Flags { + imap_flags.iter().map(from_imap_flag).collect() +} diff --git a/cli/src/backends/maildir/maildir_backend.rs b/cli/src/backends/maildir/maildir_backend.rs index 0acba73..2c4e2d0 100644 --- a/cli/src/backends/maildir/maildir_backend.rs +++ b/cli/src/backends/maildir/maildir_backend.rs @@ -7,13 +7,14 @@ use anyhow::{anyhow, Context, Result}; use himalaya_lib::{ account::{AccountConfig, MaildirBackendConfig}, mbox::{Mbox, Mboxes}, + msg::Envelopes, }; use log::{debug, info, trace}; -use std::{convert::TryInto, env, ffi::OsStr, fs, path::PathBuf}; +use std::{env, ffi::OsStr, fs, path::PathBuf}; use crate::{ - backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags}, - msg::{Envelopes, Msg}, + backends::{maildir_envelopes, Backend, IdMapper}, + msg::Msg, }; /// Represents the maildir backend. @@ -136,12 +137,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { Ok(()) } - fn get_envelopes( - &mut self, - dir: &str, - page_size: usize, - page: usize, - ) -> Result> { + fn get_envelopes(&mut self, dir: &str, page_size: usize, page: usize) -> Result { info!(">> get maildir envelopes"); debug!("dir: {:?}", dir); debug!("page size: {:?}", page_size); @@ -153,9 +149,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { // Reads envelopes from the "cur" folder of the selected // maildir. - let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().with_context(|| { - format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) - })?; + let mut envelopes = + maildir_envelopes::from_maildir_entries(mdir.list_cur()).with_context(|| { + format!("cannot parse maildir envelopes from {:?}", self.mdir.path()) + })?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -185,7 +182,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mut mapper = IdMapper::new(mdir.path())?; let entries = envelopes .iter() - .map(|env| (env.hash.to_owned(), env.id.to_owned())) + .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) .collect(); mapper.append(entries)? }; @@ -194,10 +191,10 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { // Shorten envelopes hash. envelopes .iter_mut() - .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); info!("<< get maildir envelopes"); - Ok(Box::new(envelopes)) + Ok(envelopes) } fn search_envelopes( @@ -207,7 +204,7 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { _sort: &str, _page_size: usize, _page: usize, - ) -> Result> { + ) -> Result { info!(">> search maildir envelopes"); info!("<< search maildir envelopes"); Err(anyhow!( @@ -223,9 +220,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; let id = mdir .store_cur_with_flags(msg, &flags.to_string()) .with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?; @@ -426,9 +420,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? @@ -457,9 +448,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? @@ -488,9 +476,6 @@ impl<'a> Backend<'a> for MaildirBackend<'a> { let mdir = self .get_mdir_from_dir(dir) .with_context(|| format!("cannot get maildir instance from {:?}", dir))?; - let flags: MaildirFlags = flags - .try_into() - .with_context(|| format!("cannot parse maildir flags {:?}", flags))?; debug!("flags: {:?}", flags); let id = IdMapper::new(mdir.path()) .with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))? diff --git a/cli/src/backends/maildir/maildir_envelope.rs b/cli/src/backends/maildir/maildir_envelope.rs index 10cb3be..8cc9884 100644 --- a/cli/src/backends/maildir/maildir_envelope.rs +++ b/cli/src/backends/maildir/maildir_envelope.rs @@ -1,194 +1,72 @@ -//! Maildir mailbox module. -//! -//! This module provides Maildir types and conversion utilities -//! related to the envelope - -use anyhow::{anyhow, Context, Error, Result}; +use anyhow::{anyhow, Context, Result}; use chrono::DateTime; +use himalaya_lib::msg::Envelope; use log::trace; -use std::{ - convert::{TryFrom, TryInto}, - ops::{Deref, DerefMut}, -}; use crate::{ - backends::{MaildirFlag, MaildirFlags}, + backends::maildir_flags, msg::{from_slice_to_addrs, Addr}, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, }; -/// Represents a list of envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct MaildirEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for MaildirEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl DerefMut for MaildirEnvelopes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.envelopes - } -} - -impl PrintTable for MaildirEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -// impl Envelopes for MaildirEnvelopes { -// // -// } - -/// Represents the envelope. The envelope is just a message subset, -/// and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct MaildirEnvelope { - /// Represents the id of the message. - pub id: String, - - /// Represents the MD5 hash of the message id. - pub hash: String, - - /// Represents the flags of the message. - pub flags: MaildirFlags, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the date of the message. - pub date: String, -} - -impl Table for MaildirEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("HASH").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let hash = self.hash.clone(); - let unseen = !self.flags.contains(&MaildirFlag::Seen); - let flags = self.flags.to_symbols_string(); - let subject = &self.subject; - let sender = &self.sender; - let date = &self.date; - Row::new() - .cell(Cell::new(hash).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopees returned by the `maildir` crate. -pub type RawMaildirEnvelopes = maildir::MailEntries; - -impl<'a> TryFrom for MaildirEnvelopes { - type Error = Error; - - fn try_from(mail_entries: RawMaildirEnvelopes) -> Result { - let mut envelopes = vec![]; - for entry in mail_entries { - let envelope: MaildirEnvelope = entry - .context("cannot decode maildir mail entry")? - .try_into() - .context("cannot parse maildir mail entry")?; - envelopes.push(envelope); - } - - Ok(MaildirEnvelopes { envelopes }) - } -} - /// Represents the raw envelope returned by the `maildir` crate. -pub type RawMaildirEnvelope = maildir::MailEntry; +pub type MaildirEnvelope = maildir::MailEntry; -impl<'a> TryFrom for MaildirEnvelope { - type Error = Error; +pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { + trace!(">> build envelope from maildir parsed mail"); - fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result { - trace!(">> build envelope from maildir parsed mail"); + let mut envelope = Envelope::default(); - let mut envelope = Self::default(); + envelope.internal_id = entry.id().to_owned(); + envelope.id = format!("{:x}", md5::compute(&envelope.internal_id)); + envelope.flags = maildir_flags::from_maildir_entry(&entry); - envelope.id = mail_entry.id().into(); - envelope.hash = format!("{:x}", md5::compute(&envelope.id)); - envelope.flags = (&mail_entry) - .try_into() - .context("cannot parse maildir flags")?; + let parsed_mail = entry.parsed().context("cannot parse maildir mail entry")?; - let parsed_mail = mail_entry - .parsed() - .context("cannot parse maildir mail entry")?; + trace!(">> parse headers"); + for h in parsed_mail.get_headers() { + let k = h.get_key(); + trace!("header key: {:?}", k); - trace!(">> parse headers"); - for h in parsed_mail.get_headers() { - let k = h.get_key(); - trace!("header key: {:?}", k); + let v = rfc2047_decoder::decode(h.get_value_raw()) + .context(format!("cannot decode value from header {:?}", k))?; + trace!("header value: {:?}", v); - let v = rfc2047_decoder::decode(h.get_value_raw()) - .context(format!("cannot decode value from header {:?}", k))?; - trace!("header value: {:?}", v); - - match k.to_lowercase().as_str() { - "date" => { - envelope.date = - DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0) - .context(format!("cannot parse maildir message date {:?}", v))? - .naive_local() - .to_string(); - } - "subject" => { - envelope.subject = v.into(); - } - "from" => { - envelope.sender = from_slice_to_addrs(v) - .context(format!("cannot parse header {:?}", k))? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => { - group_name.to_owned() - } - }) - .ok_or_else(|| anyhow!("cannot find sender"))?; - } - _ => (), + match k.to_lowercase().as_str() { + "date" => { + envelope.date = + DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0) + .map(|date| date.naive_local().to_string()) + .ok() } + "subject" => { + envelope.subject = v.into(); + } + "from" => { + envelope.sender = from_slice_to_addrs(v) + .context(format!("cannot parse header {:?}", k))? + .and_then(|senders| { + if senders.is_empty() { + None + } else { + Some(senders) + } + }) + .map(|senders| match &senders[0] { + Addr::Single(mailparse::SingleInfo { display_name, addr }) => { + display_name.as_ref().unwrap_or_else(|| addr).to_owned() + } + Addr::Group(mailparse::GroupInfo { group_name, .. }) => { + group_name.to_owned() + } + }) + .ok_or_else(|| anyhow!("cannot find sender"))?; + } + _ => (), } - trace!("<< parse headers"); - - trace!("envelope: {:?}", envelope); - trace!("<< build envelope from maildir parsed mail"); - Ok(envelope) } + trace!("<< parse headers"); + + trace!("envelope: {:?}", envelope); + trace!("<< build envelope from maildir parsed mail"); + Ok(envelope) } diff --git a/cli/src/backends/maildir/maildir_envelopes.rs b/cli/src/backends/maildir/maildir_envelopes.rs new file mode 100644 index 0000000..4921227 --- /dev/null +++ b/cli/src/backends/maildir/maildir_envelopes.rs @@ -0,0 +1,25 @@ +//! Maildir mailbox module. +//! +//! This module provides Maildir types and conversion utilities +//! related to the envelope + +use himalaya_lib::msg::Envelopes; +use anyhow::{Result, Context}; + +use super::maildir_envelope; + +/// Represents a list of raw envelopees returned by the `maildir` crate. +pub type MaildirEnvelopes = maildir::MailEntries; + +pub fn from_maildir_entries(mail_entries: MaildirEnvelopes) -> Result { + let mut envelopes = Envelopes::default(); + for entry in mail_entries { + envelopes.push( + maildir_envelope::from_maildir_entry( + entry.context("cannot decode maildir mail entry")?, + ) + .context("cannot parse maildir mail entry")?, + ); + } + Ok(envelopes) +} diff --git a/cli/src/backends/maildir/maildir_flag.rs b/cli/src/backends/maildir/maildir_flag.rs index 1c97cce..b868e8a 100644 --- a/cli/src/backends/maildir/maildir_flag.rs +++ b/cli/src/backends/maildir/maildir_flag.rs @@ -1,129 +1,13 @@ -use anyhow::{anyhow, Error, Result}; -use std::{ - convert::{TryFrom, TryInto}, - ops::Deref, -}; +use himalaya_lib::msg::Flag; -/// Represents the maildir flag variants. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] -pub enum MaildirFlag { - Passed, - Replied, - Seen, - Trashed, - Draft, - Flagged, - Custom(char), -} - -/// Represents the maildir flags. -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)] -pub struct MaildirFlags(pub Vec); - -impl MaildirFlags { - /// Builds a symbols string - pub fn to_symbols_string(&self) -> String { - let mut flags = String::new(); - flags.push_str(if self.contains(&MaildirFlag::Seen) { - " " - } else { - "✷" - }); - flags.push_str(if self.contains(&MaildirFlag::Replied) { - "↵" - } else { - " " - }); - flags.push_str(if self.contains(&MaildirFlag::Passed) { - "↗" - } else { - " " - }); - flags.push_str(if self.contains(&MaildirFlag::Flagged) { - "⚑" - } else { - " " - }); - flags - } -} - -impl Deref for MaildirFlags { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ToString for MaildirFlags { - fn to_string(&self) -> String { - self.0 - .iter() - .map(|flag| { - let flag_char: char = flag.into(); - flag_char - }) - .collect() - } -} - -impl TryFrom<&str> for MaildirFlags { - type Error = Error; - - fn try_from(flags_str: &str) -> Result { - let mut flags = vec![]; - for flag_str in flags_str.split_whitespace() { - flags.push(flag_str.trim().try_into()?); - } - Ok(MaildirFlags(flags)) - } -} - -impl From<&maildir::MailEntry> for MaildirFlags { - fn from(mail_entry: &maildir::MailEntry) -> Self { - let mut flags = vec![]; - for c in mail_entry.flags().chars() { - flags.push(match c { - 'P' => MaildirFlag::Passed, - 'R' => MaildirFlag::Replied, - 'S' => MaildirFlag::Seen, - 'T' => MaildirFlag::Trashed, - 'D' => MaildirFlag::Draft, - 'F' => MaildirFlag::Flagged, - custom => MaildirFlag::Custom(custom), - }) - } - Self(flags) - } -} - -impl Into for &MaildirFlag { - fn into(self) -> char { - match self { - MaildirFlag::Passed => 'P', - MaildirFlag::Replied => 'R', - MaildirFlag::Seen => 'S', - MaildirFlag::Trashed => 'T', - MaildirFlag::Draft => 'D', - MaildirFlag::Flagged => 'F', - MaildirFlag::Custom(custom) => *custom, - } - } -} - -impl TryFrom<&str> for MaildirFlag { - type Error = Error; - - fn try_from(flag_str: &str) -> Result { - match flag_str { - "passed" => Ok(MaildirFlag::Passed), - "replied" => Ok(MaildirFlag::Replied), - "seen" => Ok(MaildirFlag::Seen), - "trashed" => Ok(MaildirFlag::Trashed), - "draft" => Ok(MaildirFlag::Draft), - "flagged" => Ok(MaildirFlag::Flagged), - flag_str => Err(anyhow!("cannot parse maildir flag {:?}", flag_str)), - } +pub fn from_char(c: char) -> Flag { + match c { + 'R' => Flag::Answered, + 'S' => Flag::Seen, + 'T' => Flag::Deleted, + 'D' => Flag::Draft, + 'F' => Flag::Flagged, + 'P' => Flag::Custom(String::from("Passed")), + flag => Flag::Custom(flag.to_string()), } } diff --git a/cli/src/backends/maildir/maildir_flags.rs b/cli/src/backends/maildir/maildir_flags.rs new file mode 100644 index 0000000..538c90a --- /dev/null +++ b/cli/src/backends/maildir/maildir_flags.rs @@ -0,0 +1,7 @@ +use himalaya_lib::msg::Flags; + +use super::maildir_flag; + +pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags { + entry.flags().chars().map(maildir_flag::from_char).collect() +} diff --git a/cli/src/backends/notmuch/notmuch_backend.rs b/cli/src/backends/notmuch/notmuch_backend.rs index 8f71faf..b9bcc12 100644 --- a/cli/src/backends/notmuch/notmuch_backend.rs +++ b/cli/src/backends/notmuch/notmuch_backend.rs @@ -1,15 +1,16 @@ -use std::{convert::TryInto, fs}; +use std::fs; use anyhow::{anyhow, Context, Result}; use himalaya_lib::{ account::{AccountConfig, NotmuchBackendConfig}, mbox::{Mbox, Mboxes}, + msg::Envelopes, }; use log::{debug, info, trace}; use crate::{ - backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes}, - msg::{Envelopes, Msg}, + backends::{notmuch_envelopes, Backend, IdMapper, MaildirBackend}, + msg::Msg, }; /// Represents the Notmuch backend. @@ -53,16 +54,16 @@ impl<'a> NotmuchBackend<'a> { query: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { // Gets envelopes matching the given Notmuch query. let query_builder = self .db .create_query(query) .with_context(|| format!("cannot create notmuch query from {:?}", query))?; - let mut envelopes: NotmuchEnvelopes = query_builder - .search_messages() - .with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))? - .try_into() + let mut envelopes = + notmuch_envelopes::from_notmuch_msgs(query_builder.search_messages().with_context( + || format!("cannot find notmuch envelopes from query {:?}", query), + )?) .with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?; debug!("envelopes len: {:?}", envelopes.len()); trace!("envelopes: {:?}", envelopes); @@ -93,7 +94,7 @@ impl<'a> NotmuchBackend<'a> { let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?; let entries = envelopes .iter() - .map(|env| (env.hash.to_owned(), env.id.to_owned())) + .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) .collect(); mapper.append(entries)? }; @@ -102,9 +103,9 @@ impl<'a> NotmuchBackend<'a> { // Shorten envelopes hash. envelopes .iter_mut() - .for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned()); + .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); - Ok(Box::new(envelopes)) + Ok(envelopes) } } @@ -148,7 +149,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { virt_mbox: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { info!(">> get notmuch envelopes"); debug!("virtual mailbox: {:?}", virt_mbox); debug!("page size: {:?}", page_size); @@ -174,7 +175,7 @@ impl<'a> Backend<'a> for NotmuchBackend<'a> { _sort: &str, page_size: usize, page: usize, - ) -> Result> { + ) -> Result { info!(">> search notmuch envelopes"); debug!("virtual mailbox: {:?}", virt_mbox); debug!("query: {:?}", query); diff --git a/cli/src/backends/notmuch/notmuch_envelope.rs b/cli/src/backends/notmuch/notmuch_envelope.rs index 626d949..4d53ba2 100644 --- a/cli/src/backends/notmuch/notmuch_envelope.rs +++ b/cli/src/backends/notmuch/notmuch_envelope.rs @@ -3,178 +3,72 @@ //! This module provides Notmuch types and conversion utilities //! related to the envelope -use anyhow::{anyhow, Context, Error, Result}; +use anyhow::{anyhow, Context, Result}; use chrono::DateTime; +use himalaya_lib::msg::{Envelope, Flag}; use log::{info, trace}; -use std::{ - convert::{TryFrom, TryInto}, - ops::{Deref, DerefMut}, -}; -use crate::{ - msg::{from_slice_to_addrs, Addr}, - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents a list of envelopes. -#[derive(Debug, Default, serde::Serialize)] -pub struct NotmuchEnvelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl Deref for NotmuchEnvelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl DerefMut for NotmuchEnvelopes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.envelopes - } -} - -impl PrintTable for NotmuchEnvelopes { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -/// Represents the envelope. The envelope is just a message subset, -/// and is mostly used for listings. -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct NotmuchEnvelope { - /// Represents the id of the message. - pub id: String, - - /// Represents the MD5 hash of the message id. - pub hash: String, - - /// Represents the tags of the message. - pub flags: Vec, - - /// Represents the subject of the message. - pub subject: String, - - /// Represents the first sender of the message. - pub sender: String, - - /// Represents the date of the message. - pub date: String, -} - -impl Table for NotmuchEnvelope { - fn head() -> Row { - Row::new() - .cell(Cell::new("HASH").bold().underline().white()) - .cell(Cell::new("FLAGS").bold().underline().white()) - .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) - .cell(Cell::new("DATE").bold().underline().white()) - } - - fn row(&self) -> Row { - let hash = self.hash.to_string(); - let unseen = !self.flags.contains(&String::from("unread")); - let flags = String::new(); - let subject = &self.subject; - let sender = &self.sender; - let date = &self.date; - Row::new() - .cell(Cell::new(hash).bold_if(unseen).red()) - .cell(Cell::new(flags).bold_if(unseen).white()) - .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) - .cell(Cell::new(sender).bold_if(unseen).blue()) - .cell(Cell::new(date).bold_if(unseen).yellow()) - } -} - -/// Represents a list of raw envelopees returned by the `notmuch` crate. -pub type RawNotmuchEnvelopes = notmuch::Messages; - -impl<'a> TryFrom for NotmuchEnvelopes { - type Error = Error; - - fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result { - let mut envelopes = vec![]; - for raw_envelope in raw_envelopes { - let envelope: NotmuchEnvelope = raw_envelope - .try_into() - .context("cannot parse notmuch mail entry")?; - envelopes.push(envelope); - } - Ok(NotmuchEnvelopes { envelopes }) - } -} +use crate::msg::{from_slice_to_addrs, Addr}; /// Represents the raw envelope returned by the `notmuch` crate. pub type RawNotmuchEnvelope = notmuch::Message; -impl<'a> TryFrom for NotmuchEnvelope { - type Error = Error; +pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { + info!("begin: try building envelope from notmuch parsed mail"); - fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result { - info!("begin: try building envelope from notmuch parsed mail"); + let internal_id = raw_envelope.id().to_string(); + let id = format!("{:x}", md5::compute(&internal_id)); + let subject = raw_envelope + .header("subject") + .context("cannot get header \"Subject\" from notmuch message")? + .unwrap_or_default() + .to_string(); + let sender = raw_envelope + .header("from") + .context("cannot get header \"From\" from notmuch message")? + .ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", internal_id))? + .to_string(); + let sender = from_slice_to_addrs(sender)? + .and_then(|senders| { + if senders.is_empty() { + None + } else { + Some(senders) + } + }) + .map(|senders| match &senders[0] { + Addr::Single(mailparse::SingleInfo { display_name, addr }) => { + display_name.as_ref().unwrap_or_else(|| addr).to_owned() + } + Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), + }) + .ok_or_else(|| anyhow!("cannot find sender"))?; + let date = raw_envelope + .header("date") + .context("cannot get header \"Date\" from notmuch message")? + .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", internal_id))? + .to_string(); + let date = DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) + .context(format!( + "cannot parse message date {:?} of notmuch message {:?}", + date, internal_id + )) + .map(|date| date.naive_local().to_string()) + .ok(); - let id = raw_envelope.id().to_string(); - let hash = format!("{:x}", md5::compute(&id)); - let subject = raw_envelope - .header("subject") - .context("cannot get header \"Subject\" from notmuch message")? - .unwrap_or_default() - .to_string(); - let sender = raw_envelope - .header("from") - .context("cannot get header \"From\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", id))? - .to_string(); - let sender = from_slice_to_addrs(sender)? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), - }) - .ok_or_else(|| anyhow!("cannot find sender"))?; - let date = raw_envelope - .header("date") - .context("cannot get header \"Date\" from notmuch message")? - .ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", id))? - .to_string(); - let date = - DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) - .context(format!( - "cannot parse message date {:?} of notmuch message {:?}", - date, id - ))? - .naive_local() - .to_string(); + let envelope = Envelope { + id, + internal_id, + flags: raw_envelope + .tags() + .map(|tag| Flag::Custom(tag.to_string())) + .collect(), + subject, + sender, + date, + }; + trace!("envelope: {:?}", envelope); - let envelope = Self { - id, - hash, - flags: raw_envelope.tags().collect(), - subject, - sender, - date, - }; - trace!("envelope: {:?}", envelope); - - info!("end: try building envelope from notmuch parsed mail"); - Ok(envelope) - } + info!("end: try building envelope from notmuch parsed mail"); + Ok(envelope) } diff --git a/cli/src/backends/notmuch/notmuch_envelopes.rs b/cli/src/backends/notmuch/notmuch_envelopes.rs new file mode 100644 index 0000000..7235a46 --- /dev/null +++ b/cli/src/backends/notmuch/notmuch_envelopes.rs @@ -0,0 +1,18 @@ +use anyhow::{Context, Result}; +use himalaya_lib::msg::Envelopes; + +use super::notmuch_envelope; + +/// Represents a list of raw envelopees returned by the `notmuch` +/// crate. +pub type RawNotmuchEnvelopes = notmuch::Messages; + +pub fn from_notmuch_msgs(msgs: RawNotmuchEnvelopes) -> Result { + let mut envelopes = Envelopes::default(); + for msg in msgs { + let envelope = + notmuch_envelope::from_notmuch_msg(msg).context("cannot parse notmuch message")?; + envelopes.push(envelope); + } + Ok(envelopes) +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 144794f..634a52f 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -13,6 +13,9 @@ pub mod msg { pub mod envelope; pub use envelope::*; + pub mod envelopes; + pub use envelopes::*; + pub mod msg_args; pub mod msg_handlers; @@ -52,9 +55,15 @@ pub mod backends { pub mod imap_handlers; + pub mod imap_envelopes; + pub use imap_envelopes::*; + pub mod imap_envelope; pub use imap_envelope::*; + pub mod imap_flags; + pub use imap_flags::*; + pub mod imap_flag; pub use imap_flag::*; @@ -69,9 +78,15 @@ pub mod backends { pub mod maildir_backend; pub use maildir_backend::*; + pub mod maildir_envelopes; + pub use maildir_envelopes::*; + pub mod maildir_envelope; pub use maildir_envelope::*; + pub mod maildir_flags; + pub use maildir_flags::*; + pub mod maildir_flag; pub use maildir_flag::*; } @@ -84,6 +99,9 @@ pub mod backends { pub mod notmuch_backend; pub use notmuch_backend::*; + pub mod notmuch_envelopes; + pub use notmuch_envelopes::*; + pub mod notmuch_envelope; pub use notmuch_envelope::*; } diff --git a/cli/src/msg/envelope.rs b/cli/src/msg/envelope.rs index 91c5c04..2f96dd9 100644 --- a/cli/src/msg/envelope.rs +++ b/cli/src/msg/envelope.rs @@ -1,13 +1,30 @@ -use std::{any, fmt}; +use himalaya_lib::msg::{Envelope, Flag}; -use crate::output::PrintTable; +use crate::ui::{Cell, Row, Table}; -pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any { - fn as_any(&self) -> &dyn any::Any; -} +impl Table for Envelope { + fn head() -> Row { + Row::new() + .cell(Cell::new("ID").bold().underline().white()) + .cell(Cell::new("FLAGS").bold().underline().white()) + .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) + .cell(Cell::new("SENDER").bold().underline().white()) + .cell(Cell::new("DATE").bold().underline().white()) + } -impl Envelopes for T { - fn as_any(&self) -> &dyn any::Any { - self + fn row(&self) -> Row { + let id = self.id.to_string(); + let flags = self.flags.to_symbols_string(); + let unseen = !self.flags.contains(&Flag::Seen); + let subject = &self.subject; + let sender = &self.sender; + let date = self.date.as_deref().unwrap_or_default(); + + Row::new() + .cell(Cell::new(id).bold_if(unseen).red()) + .cell(Cell::new(flags).bold_if(unseen).white()) + .cell(Cell::new(subject).shrinkable().bold_if(unseen).green()) + .cell(Cell::new(sender).bold_if(unseen).blue()) + .cell(Cell::new(date).bold_if(unseen).yellow()) } } diff --git a/cli/src/msg/envelopes.rs b/cli/src/msg/envelopes.rs new file mode 100644 index 0000000..0a524a9 --- /dev/null +++ b/cli/src/msg/envelopes.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use himalaya_lib::msg::Envelopes; + +use crate::{ + output::{PrintTable, PrintTableOpts, WriteColor}, + ui::Table, +}; + +impl PrintTable for Envelopes { + fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { + writeln!(writer)?; + Table::print(writer, self, opts)?; + writeln!(writer)?; + Ok(()) + } +} diff --git a/cli/src/msg/msg_handlers.rs b/cli/src/msg/msg_handlers.rs index 0c2e0c0..3011427 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/cli/src/msg/msg_handlers.rs @@ -126,7 +126,7 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = imap.get_envelopes(mbox, page_size, page)?; trace!("envelopes: {:?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, @@ -310,7 +310,7 @@ pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?; trace!("messages: {:#?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, @@ -335,7 +335,7 @@ pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?; trace!("envelopes: {:#?}", msgs); printer.print_table( - msgs, + Box::new(msgs), PrintTableOpts { format: &config.format, max_width, diff --git a/lib/Cargo.toml b/lib/Cargo.toml index fecd793..b28fb40 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" imap-backend = ["imap", "imap-proto"] maildir-backend = ["maildir", "md5"] notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend", "notmuch-backend"] +default = ["imap-backend", "maildir-backend"] [dependencies] lettre = { version = "0.10.0-rc.6", features = ["serde"] } diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 87eb169..d8398a8 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -2,3 +2,4 @@ mod process; pub mod account; pub mod mbox; +pub mod msg; diff --git a/lib/src/msg/envelope.rs b/lib/src/msg/envelope.rs new file mode 100644 index 0000000..cc04ee3 --- /dev/null +++ b/lib/src/msg/envelope.rs @@ -0,0 +1,21 @@ +use serde::Serialize; + +use super::Flags; + +/// Represents the message envelope. The envelope is just a message +/// subset, and is mostly used for listings. +#[derive(Debug, Default, Clone, Serialize)] +pub struct Envelope { + /// Represents the message identifier. + pub id: String, + /// Represents the internal message identifier. + pub internal_id: String, + /// Represents the message flags. + pub flags: Flags, + /// Represents the subject of the message. + pub subject: String, + /// Represents the first sender of the message. + pub sender: String, + /// Represents the internal date of the message. + pub date: Option, +} diff --git a/lib/src/msg/envelopes.rs b/lib/src/msg/envelopes.rs new file mode 100644 index 0000000..9cf85c9 --- /dev/null +++ b/lib/src/msg/envelopes.rs @@ -0,0 +1,25 @@ +use serde::Serialize; +use std::ops; + +use super::Envelope; + +/// Represents the list of envelopes. +#[derive(Debug, Default, Serialize)] +pub struct Envelopes { + #[serde(rename = "response")] + pub envelopes: Vec, +} + +impl ops::Deref for Envelopes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.envelopes + } +} + +impl ops::DerefMut for Envelopes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.envelopes + } +} diff --git a/lib/src/msg/flag.rs b/lib/src/msg/flag.rs new file mode 100644 index 0000000..1d37e18 --- /dev/null +++ b/lib/src/msg/flag.rs @@ -0,0 +1,27 @@ +use serde::Serialize; + +/// Represents the flag variants. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum Flag { + Seen, + Answered, + Flagged, + Deleted, + Draft, + Recent, + Custom(String), +} + +impl From<&str> for Flag { + fn from(flag_str: &str) -> Self { + match flag_str { + "seen" => Flag::Seen, + "answered" | "replied" => Flag::Answered, + "flagged" => Flag::Flagged, + "deleted" | "trashed" => Flag::Deleted, + "draft" => Flag::Draft, + "recent" => Flag::Recent, + flag => Flag::Custom(flag.into()), + } + } +} diff --git a/lib/src/msg/flags.rs b/lib/src/msg/flags.rs new file mode 100644 index 0000000..50db6ce --- /dev/null +++ b/lib/src/msg/flags.rs @@ -0,0 +1,89 @@ +use std::{fmt, ops}; + +use serde::Serialize; + +use super::Flag; + +/// Represents the list of flags. +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] +pub struct Flags(pub Vec); + +impl Flags { + /// Builds a symbols string. + pub fn to_symbols_string(&self) -> String { + let mut flags = String::new(); + flags.push_str(if self.contains(&Flag::Seen) { + " " + } else { + "✷" + }); + flags.push_str(if self.contains(&Flag::Answered) { + "↵" + } else { + " " + }); + flags.push_str(if self.contains(&Flag::Flagged) { + "⚑" + } else { + " " + }); + flags + } +} + +impl ops::Deref for Flags { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for Flags { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Display for Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut glue = ""; + + for flag in &self.0 { + write!(f, "{}", glue)?; + match flag { + Flag::Seen => write!(f, "\\Seen")?, + Flag::Answered => write!(f, "\\Answered")?, + Flag::Flagged => write!(f, "\\Flagged")?, + Flag::Deleted => write!(f, "\\Deleted")?, + Flag::Draft => write!(f, "\\Draft")?, + Flag::Recent => write!(f, "\\Recent")?, + Flag::Custom(flag) => write!(f, "{}", flag)?, + } + glue = " "; + } + + Ok(()) + } +} + +impl From<&str> for Flags { + fn from(flags: &str) -> Self { + Flags( + flags + .split_whitespace() + .map(|flag| flag.trim().into()) + .collect(), + ) + } +} + +impl FromIterator for Flags { + fn from_iter>(iter: T) -> Self { + let mut flags = Flags::default(); + for flag in iter { + flags.push(flag); + } + flags + } +} diff --git a/lib/src/msg/mod.rs b/lib/src/msg/mod.rs new file mode 100644 index 0000000..d3bec40 --- /dev/null +++ b/lib/src/msg/mod.rs @@ -0,0 +1,11 @@ +mod flag; +pub use flag::*; + +mod flags; +pub use flags::*; + +mod envelope; +pub use envelope::*; + +mod envelopes; +pub use envelopes::*;