move envelopes and flags to lib

refactor maildir envelopes/flags

refactor notmuch envelopes
This commit is contained in:
Clément DOUIN 2022-06-04 12:34:03 +02:00
parent ca67780341
commit 8f667def0c
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
26 changed files with 578 additions and 879 deletions

View file

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

View file

@ -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<Mboxes>;
fn del_mbox(&mut self, mbox: &str) -> Result<()>;
fn get_envelopes(
&mut self,
mbox: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>>;
fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result<Envelopes>;
fn search_envelopes(
&mut self,
mbox: &str,
@ -29,7 +24,7 @@ pub trait Backend<'a> {
sort: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>>;
) -> Result<Envelopes>;
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<String>;
fn get_msg(&mut self, mbox: &str, id: &str) -> Result<Msg>;
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;

View file

@ -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<TlsStream<TcpStream>>;
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<Box<dyn Envelopes>> {
fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result<Envelopes> {
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<Box<dyn Envelopes>> {
) -> Result<Envelopes> {
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<String> {
let flags: ImapFlags = flags.into();
let flags: Flags = flags.into();
self.sess()?
.append(mbox, msg)
.flags(<ImapFlags as Into<Vec<imap::types::Flag<'a>>>>::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))?;

View file

@ -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<ImapEnvelope>,
}
impl Deref for ImapEnvelopes {
type Target = Vec<ImapEnvelope>;
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<String>,
}
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<Vec<RawImapEnvelope>>;
impl TryFrom<RawImapEnvelopes> for ImapEnvelopes {
type Error = Error;
fn try_from(raw_envelopes: RawImapEnvelopes) -> Result<Self, Self::Error> {
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<Envelope> {
let envelope = fetch
.envelope()
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
fn try_from(fetch: &RawImapEnvelope) -> Result<ImapEnvelope> {
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,
})
}

View file

@ -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<Vec<ImapFetch>>;
pub fn from_imap_fetches(fetches: ImapFetches) -> Result<Envelopes> {
let mut envelopes = Envelopes::default();
for fetch in fetches.iter().rev() {
envelopes.push(from_imap_fetch(fetch).context("cannot parse imap fetch")?);
}
Ok(envelopes)
}

View file

@ -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<Self, Self::Error> {
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<ImapFlag>);
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<ImapFlag>;
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<Vec<imap::types::Flag<'a>>> for ImapFlags {
fn into(self) -> Vec<imap::types::Flag<'a>> {
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<Self, Self::Error> {
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()),
}
}

View file

@ -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<imap::types::Flag<'a>> {
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()
}

View file

@ -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<Box<dyn Envelopes>> {
fn get_envelopes(&mut self, dir: &str, page_size: usize, page: usize) -> Result<Envelopes> {
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<Box<dyn Envelopes>> {
) -> Result<Envelopes> {
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()))?

View file

@ -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<MaildirEnvelope>,
}
impl Deref for MaildirEnvelopes {
type Target = Vec<MaildirEnvelope>;
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<RawMaildirEnvelopes> for MaildirEnvelopes {
type Error = Error;
fn try_from(mail_entries: RawMaildirEnvelopes) -> Result<Self, Self::Error> {
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<RawMaildirEnvelope> for MaildirEnvelope {
type Error = Error;
pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result<Envelope> {
trace!(">> build envelope from maildir parsed mail");
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
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)
}

View file

@ -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<Envelopes> {
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)
}

View file

@ -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<MaildirFlag>);
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<MaildirFlag>;
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<Self, Self::Error> {
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<char> 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<Self, Self::Error> {
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()),
}
}

View file

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

View file

@ -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<Box<dyn Envelopes>> {
) -> Result<Envelopes> {
// 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<Box<dyn Envelopes>> {
) -> Result<Envelopes> {
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<Box<dyn Envelopes>> {
) -> Result<Envelopes> {
info!(">> search notmuch envelopes");
debug!("virtual mailbox: {:?}", virt_mbox);
debug!("query: {:?}", query);

View file

@ -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<NotmuchEnvelope>,
}
impl Deref for NotmuchEnvelopes {
type Target = Vec<NotmuchEnvelope>;
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<String>,
/// 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<RawNotmuchEnvelopes> for NotmuchEnvelopes {
type Error = Error;
fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result<Self, Self::Error> {
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<RawNotmuchEnvelope> for NotmuchEnvelope {
type Error = Error;
pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result<Envelope> {
info!("begin: try building envelope from notmuch parsed mail");
fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result<Self, Self::Error> {
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)
}

View file

@ -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<Envelopes> {
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)
}

View file

@ -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::*;
}

View file

@ -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<T: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any> 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())
}
}

16
cli/src/msg/envelopes.rs Normal file
View file

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

View file

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

View file

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

View file

@ -2,3 +2,4 @@ mod process;
pub mod account;
pub mod mbox;
pub mod msg;

21
lib/src/msg/envelope.rs Normal file
View file

@ -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<String>,
}

25
lib/src/msg/envelopes.rs Normal file
View file

@ -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<Envelope>,
}
impl ops::Deref for Envelopes {
type Target = Vec<Envelope>;
fn deref(&self) -> &Self::Target {
&self.envelopes
}
}
impl ops::DerefMut for Envelopes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.envelopes
}
}

27
lib/src/msg/flag.rs Normal file
View file

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

89
lib/src/msg/flags.rs Normal file
View file

@ -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<Flag>);
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<Flag>;
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<Flag> for Flags {
fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
let mut flags = Flags::default();
for flag in iter {
flags.push(flag);
}
flags
}
}

11
lib/src/msg/mod.rs Normal file
View file

@ -0,0 +1,11 @@
mod flag;
pub use flag::*;
mod flags;
pub use flags::*;
mod envelope;
pub use envelope::*;
mod envelopes;
pub use envelopes::*;