release v0.5.6 (#301)

* make use of mailparse::MailAddr

* move addr logic to a dedicated file

* update changelog

* add suffix to downoalded attachments with same name (#204)

* implement sort command (#34)

* introduce backends structure (#296)

* implement backend structure poc

* improve config namings

* improve account namings and structure

* rename imap vars to backend

* maildir backend (#299)

* refactor config system, preparing maildir backend

* rename deserializable by deserialized

* wrap backend in a Box

* reword backend trait methods

* merge list envelopes functions

* remove find_raw_msg from backend trait

* remove expunge fn from backend trait

* rename add_msg from backend trait

* init maildir integration tests, start impl maildir backend fns

* implement remaining methods maildir backend, refactor trait

* improve backend trait, add copy and move fns

* remove usage of Mbox in handlers

* reorganize backends folder structure

* move mbox out of domain folder

* rename mbox entities

* improve mbox structure

* remove unused files, move smtp module

* improve envelope, impl get_envelopes for maildir

* link maildir mail entry id to envelope id

* use erased-serde to make backend get_mboxes return a trait object

* remove unused mbox files

* rename Output trait

* make get_envelopes return a trait object

* remove unused impl for imap envelope

* update backend return signature with Box

* replace impl from imap::Fetch to mailparse::ParsedMail

* split flags by backends

* remove unused flags from msg

* remove remaining flags from domain

* impl maildir copy and move, improve maildir e2e tests

* set up imap backend e2e tests

* move domain/msg to msg

* repair broken tests

* fix maildir envelopes encoding issues

* add date column to maildir envelopes

* implement maildir list pagination

* improve maildir subdir path management

* add pgp and maildir features to readme

* update changelog

* bump version v0.5.6
This commit is contained in:
Clément DOUIN
2022-02-22 16:54:39 +01:00
committed by GitHub
parent 585fa77af5
commit 158bc86cfa
68 changed files with 3834 additions and 2696 deletions
+41
View File
@@ -0,0 +1,41 @@
//! Backend module.
//!
//! This module exposes the backend trait, which can be used to create
//! custom backend implementations.
use anyhow::Result;
use crate::{
mbox::Mboxes,
msg::{Envelopes, Msg},
};
pub trait Backend<'a> {
fn connect(&mut self) -> Result<()> {
Ok(())
}
fn add_mbox(&mut self, mbox: &str) -> Result<()>;
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>>;
fn del_mbox(&mut self, mbox: &str) -> Result<()>;
fn get_envelopes(
&mut self,
mbox: &str,
sort: &str,
filter: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>>;
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>>;
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<()>;
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
fn del_msg(&mut self, mbox: &str, ids: &str) -> Result<()>;
fn add_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn set_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn del_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn disconnect(&mut self) -> Result<()> {
Ok(())
}
}
+66
View File
@@ -0,0 +1,66 @@
//! Module related to IMAP CLI.
//!
//! This module provides subcommands and a command matcher related to IMAP.
use anyhow::Result;
use clap::{App, ArgMatches};
use log::{debug, info};
type Keepalive = u64;
/// IMAP commands.
pub enum Command {
/// Start the IMAP notify mode with the give keepalive duration.
Notify(Keepalive),
/// Start the IMAP watch mode with the give keepalive duration.
Watch(Keepalive),
}
/// IMAP command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Command>> {
info!("entering imap command matcher");
if let Some(m) = m.subcommand_matches("notify") {
info!("notify command matched");
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
debug!("keepalive: {}", keepalive);
return Ok(Some(Command::Notify(keepalive)));
}
if let Some(m) = m.subcommand_matches("watch") {
info!("watch command matched");
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
debug!("keepalive: {}", keepalive);
return Ok(Some(Command::Watch(keepalive)));
}
Ok(None)
}
/// IMAP subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![
clap::SubCommand::with_name("notify")
.about("Notifies when new messages arrive in the given mailbox")
.aliases(&["idle"])
.arg(
clap::Arg::with_name("keepalive")
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
clap::SubCommand::with_name("watch")
.about("Watches IMAP server changes")
.arg(
clap::Arg::with_name("keepalive")
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
]
}
+369
View File
@@ -0,0 +1,369 @@
//! IMAP backend module.
//!
//! This module contains the definition of the IMAP backend.
use anyhow::{anyhow, Context, Result};
use log::{debug, log_enabled, trace, Level};
use native_tls::{TlsConnector, TlsStream};
use std::{
collections::HashSet,
convert::{TryFrom, TryInto},
net::TcpStream,
thread,
};
use crate::{
backends::{
imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes,
},
config::{AccountConfig, ImapBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
output::run_cmd,
};
use super::ImapFlags;
type ImapSess = imap::Session<TlsStream<TcpStream>>;
pub struct ImapBackend<'a> {
account_config: &'a AccountConfig,
imap_config: &'a ImapBackendConfig,
sess: Option<ImapSess>,
}
impl<'a> ImapBackend<'a> {
pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self {
Self {
account_config,
imap_config,
sess: None,
}
}
fn sess(&mut self) -> Result<&mut ImapSess> {
if self.sess.is_none() {
debug!("create TLS builder");
debug!("insecure: {}", self.imap_config.imap_insecure);
let builder = TlsConnector::builder()
.danger_accept_invalid_certs(self.imap_config.imap_insecure)
.danger_accept_invalid_hostnames(self.imap_config.imap_insecure)
.build()
.context("cannot create TLS connector")?;
debug!("create client");
debug!("host: {}", self.imap_config.imap_host);
debug!("port: {}", self.imap_config.imap_port);
debug!("starttls: {}", self.imap_config.imap_starttls);
let mut client_builder =
imap::ClientBuilder::new(&self.imap_config.imap_host, self.imap_config.imap_port);
if self.imap_config.imap_starttls {
client_builder.starttls();
}
let client = client_builder
.connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?))
.context("cannot connect to IMAP server")?;
debug!("create session");
debug!("login: {}", self.imap_config.imap_login);
debug!("passwd cmd: {}", self.imap_config.imap_passwd_cmd);
let mut sess = client
.login(
&self.imap_config.imap_login,
&self.imap_config.imap_passwd()?,
)
.map_err(|res| res.0)
.context("cannot login to IMAP server")?;
sess.debug = log_enabled!(Level::Trace);
self.sess = Some(sess);
}
match self.sess {
Some(ref mut sess) => Ok(sess),
None => Err(anyhow!("cannot get IMAP session")),
}
}
fn search_new_msgs(&mut self, query: &str) -> Result<Vec<u32>> {
let uids: Vec<u32> = self
.sess()?
.uid_search(query)
.context("cannot search new messages")?
.into_iter()
.collect();
debug!("found {} new messages", uids.len());
trace!("uids: {:?}", uids);
Ok(uids)
}
pub fn notify(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
debug!("notify");
debug!("examine mailbox {:?}", mbox);
self.sess()?
.examine(mbox)
.context(format!("cannot examine mailbox {}", mbox))?;
debug!("init messages hashset");
let mut msgs_set: HashSet<u32> = self
.search_new_msgs(&self.account_config.notify_query)?
.iter()
.cloned()
.collect::<HashSet<_>>();
trace!("messages hashset: {:?}", msgs_set);
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
let uids: Vec<u32> = self
.search_new_msgs(&self.account_config.notify_query)?
.into_iter()
.filter(|uid| -> bool { msgs_set.get(uid).is_none() })
.collect();
debug!("found {} new messages not in hashset", uids.len());
trace!("messages hashet: {:?}", msgs_set);
if !uids.is_empty() {
let uids = uids
.iter()
.map(|uid| uid.to_string())
.collect::<Vec<_>>()
.join(",");
let fetches = self
.sess()?
.uid_fetch(uids, "(UID ENVELOPE)")
.context("cannot fetch new messages enveloppe")?;
for fetch in fetches.iter() {
let msg = ImapEnvelope::try_from(fetch)?;
let uid = fetch.uid.ok_or_else(|| {
anyhow!("cannot retrieve message {}'s UID", fetch.message)
})?;
let from = msg.sender.to_owned().into();
self.account_config.run_notify_cmd(&msg.subject, &from)?;
debug!("notify message: {}", uid);
trace!("message: {:?}", msg);
debug!("insert message {} in hashset", uid);
msgs_set.insert(uid);
trace!("messages hashset: {:?}", msgs_set);
}
}
debug!("end loop");
}
}
pub fn watch(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
debug!("examine mailbox: {}", mbox);
self.sess()?
.examine(mbox)
.context(format!("cannot examine mailbox `{}`", mbox))?;
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
let cmds = self.account_config.watch_cmds.clone();
thread::spawn(move || {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
debug!("{:?}", res);
})
});
debug!("end loop");
}
}
}
impl<'a> Backend<'a> for ImapBackend<'a> {
fn add_mbox(&mut self, mbox: &str) -> Result<()> {
self.sess()?
.create(mbox)
.context(format!("cannot create imap mailbox {:?}", mbox))
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
let mboxes: ImapMboxes = self
.sess()?
.list(Some(""), Some("*"))
.context("cannot list mailboxes")?
.into();
Ok(Box::new(mboxes))
}
fn del_mbox(&mut self, mbox: &str) -> Result<()> {
self.sess()?
.delete(mbox)
.context(format!("cannot delete imap mailbox {:?}", mbox))
}
fn get_envelopes(
&mut self,
mbox: &str,
sort: &str,
filter: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
let last_seq = self
.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?
.exists;
if last_seq == 0 {
return Ok(Box::new(ImapEnvelopes::default()));
}
let sort: SortCriteria = sort.try_into()?;
let charset = imap::extensions::sort::SortCharset::Utf8;
let begin = page * page_size;
let end = begin + (page_size - 1);
let seqs: Vec<String> = self
.sess()?
.sort(&sort, charset, filter)
.context(format!(
"cannot search in {:?} with query {:?}",
mbox, filter
))?
.iter()
.map(|seq| seq.to_string())
.collect();
if seqs.is_empty() {
return Ok(Box::new(ImapEnvelopes::default()));
}
let range = seqs[begin..end.min(seqs.len())].join(",");
let fetches = self
.sess()?
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(format!("cannot fetch messages within range {:?}", range))?;
let envelopes: ImapEnvelopes = fetches.try_into()?;
Ok(Box::new(envelopes))
}
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
let flags: ImapFlags = flags.into();
self.sess()?
.append(mbox, msg)
.flags(<ImapFlags as Into<Vec<imap::types::Flag<'a>>>>::into(flags))
.finish()
.context(format!("cannot append message to {:?}", mbox))?;
let last_seq = self
.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?
.exists;
Ok(Box::new(last_seq))
}
fn get_msg(&mut self, mbox: &str, seq: &str) -> Result<Msg> {
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
let fetches = self
.sess()?
.fetch(seq, "(FLAGS INTERNALDATE BODY[])")
.context(format!("cannot fetch messages {:?}", seq))?;
let fetch = fetches
.first()
.ok_or_else(|| anyhow!("cannot find message {:?}", seq))?;
let msg_raw = fetch.body().unwrap_or_default().to_owned();
let mut msg = Msg::from_parsed_mail(
mailparse::parse_mail(&msg_raw).context("cannot parse message")?,
self.account_config,
)?;
msg.raw = msg_raw;
Ok(msg)
}
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
let msg = self.get_msg(&mbox_src, seq)?.raw;
println!("raw: {:?}", String::from_utf8(msg.to_vec()).unwrap());
self.add_msg(&mbox_dst, &msg, "seen")?;
Ok(())
}
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
let msg = self.get_msg(mbox_src, seq)?.raw;
self.add_flags(mbox_src, seq, "seen deleted")?;
self.add_msg(&mbox_dst, &msg, "seen")?;
Ok(())
}
fn del_msg(&mut self, mbox: &str, seq: &str) -> Result<()> {
self.add_flags(mbox, seq, "deleted")
}
fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("+FLAGS ({})", flags))
.context(format!("cannot add flags {:?}", &flags))?;
self.sess()?
.expunge()
.context(format!("cannot expunge mailbox {:?}", mbox))?;
Ok(())
}
fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("FLAGS ({})", flags))
.context(format!("cannot set flags {:?}", &flags))?;
Ok(())
}
fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("-FLAGS ({})", flags))
.context(format!("cannot remove flags {:?}", &flags))?;
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
if let Some(ref mut sess) = self.sess {
debug!("logout from IMAP server");
sess.logout().context("cannot logout from IMAP server")?;
}
Ok(())
}
}
+184
View File
@@ -0,0 +1,184 @@
//! IMAP envelope module.
//!
//! 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 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(pub Vec<ImapEnvelope>);
impl Deref for ImapEnvelopes {
type Target = Vec<ImapEnvelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for ImapEnvelopes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
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))
}
}
/// Represents the raw envelope returned by the `imap` crate.
pub type RawImapEnvelope = imap::types::Fetch;
impl TryFrom<&RawImapEnvelope> for ImapEnvelope {
type Error = Error;
fn try_from(fetch: &RawImapEnvelope) -> Result<ImapEnvelope> {
let envelope = fetch
.envelope()
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
// Get the sequence number
let id = fetch.message;
// 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,
})
}
}
+147
View File
@@ -0,0 +1,147 @@
use anyhow::{anyhow, Error, Result};
use std::{convert::TryFrom, fmt, ops::Deref};
/// 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" => ImapFlag::Answered,
"flagged" => ImapFlag::Flagged,
"deleted" => 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))
}
}
+15
View File
@@ -0,0 +1,15 @@
//! Module related to IMAP handling.
//!
//! This module gathers all IMAP handlers triggered by the CLI.
use anyhow::Result;
use crate::backends::ImapBackend;
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
imap.notify(keepalive, mbox)
}
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
imap.watch(keepalive, mbox)
}
+148
View File
@@ -0,0 +1,148 @@
//! IMAP mailbox module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the mailbox.
use anyhow::Result;
use std::fmt::{self, Display};
use std::ops::Deref;
use crate::mbox::Mboxes;
use crate::{
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
use super::ImapMboxAttrs;
/// Represents a list of IMAP mailboxes.
#[derive(Debug, Default, serde::Serialize)]
pub struct ImapMboxes(pub Vec<ImapMbox>);
impl Deref for ImapMboxes {
type Target = Vec<ImapMbox>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for ImapMboxes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
impl Mboxes for ImapMboxes {
//
}
/// Represents the IMAP mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct ImapMbox {
/// Represents the mailbox hierarchie delimiter.
pub delim: String,
/// Represents the mailbox name.
pub name: String,
/// Represents the mailbox attributes.
pub attrs: ImapMboxAttrs,
}
impl ImapMbox {
pub fn new(name: &str) -> Self {
Self {
name: name.into(),
..Self::default()
}
}
}
impl Display for ImapMbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for ImapMbox {
fn head() -> Row {
Row::new()
.cell(Cell::new("DELIM").bold().underline().white())
.cell(Cell::new("NAME").bold().underline().white())
.cell(
Cell::new("ATTRIBUTES")
.shrinkable()
.bold()
.underline()
.white(),
)
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.delim).white())
.cell(Cell::new(&self.name).green())
.cell(Cell::new(&self.attrs.to_string()).shrinkable().blue())
}
}
#[cfg(test)]
mod tests {
use crate::backends::ImapMboxAttr;
use super::*;
#[test]
fn it_should_create_new_mbox() {
assert_eq!(ImapMbox::default(), ImapMbox::new(""));
assert_eq!(
ImapMbox {
name: "INBOX".into(),
..ImapMbox::default()
},
ImapMbox::new("INBOX")
);
}
#[test]
fn it_should_display_mbox() {
let default_mbox = ImapMbox::default();
assert_eq!("", default_mbox.to_string());
let new_mbox = ImapMbox::new("INBOX");
assert_eq!("INBOX", new_mbox.to_string());
let full_mbox = ImapMbox {
delim: ".".into(),
name: "Sent".into(),
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
};
assert_eq!("Sent", full_mbox.to_string());
}
}
/// Represents a list of raw mailboxes returned by the `imap` crate.
pub type RawImapMboxes = imap::types::ZeroCopy<Vec<RawImapMbox>>;
impl<'a> From<RawImapMboxes> for ImapMboxes {
fn from(raw_mboxes: RawImapMboxes) -> Self {
Self(raw_mboxes.iter().map(ImapMbox::from).collect())
}
}
/// Represents the raw mailbox returned by the `imap` crate.
pub type RawImapMbox = imap::types::Name;
impl<'a> From<&'a RawImapMbox> for ImapMbox {
fn from(raw_mbox: &'a RawImapMbox) -> Self {
Self {
delim: raw_mbox.delimiter().unwrap_or_default().into(),
name: raw_mbox.name().into(),
attrs: raw_mbox.attributes().into(),
}
}
}
+119
View File
@@ -0,0 +1,119 @@
//! IMAP mailbox attribute module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the mailbox attribute.
/// Represents the raw mailbox attribute returned by the `imap` crate.
pub use imap::types::NameAttribute as RawImapMboxAttr;
use std::{
fmt::{self, Display},
ops::Deref,
};
/// Represents the attributes of the mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct ImapMboxAttrs(pub Vec<ImapMboxAttr>);
impl Deref for ImapMboxAttrs {
type Target = Vec<ImapMboxAttr>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for ImapMboxAttrs {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut glue = "";
for attr in self.iter() {
write!(f, "{}{}", glue, attr)?;
glue = ", ";
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
pub enum ImapMboxAttr {
NoInferiors,
NoSelect,
Marked,
Unmarked,
Custom(String),
}
/// Makes the attribute displayable.
impl Display for ImapMboxAttr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ImapMboxAttr::NoInferiors => write!(f, "NoInferiors"),
ImapMboxAttr::NoSelect => write!(f, "NoSelect"),
ImapMboxAttr::Marked => write!(f, "Marked"),
ImapMboxAttr::Unmarked => write!(f, "Unmarked"),
ImapMboxAttr::Custom(custom) => write!(f, "{}", custom),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_display_attrs() {
macro_rules! attrs_from {
($($attr:expr),*) => {
ImapMboxAttrs(vec![$($attr,)*]).to_string()
};
}
let empty_attr = attrs_from![];
let single_attr = attrs_from![ImapMboxAttr::NoInferiors];
let multiple_attrs = attrs_from![
ImapMboxAttr::Custom("AttrCustom".into()),
ImapMboxAttr::NoInferiors
];
assert_eq!("", empty_attr);
assert_eq!("NoInferiors", single_attr);
assert!(multiple_attrs.contains("NoInferiors"));
assert!(multiple_attrs.contains("AttrCustom"));
assert!(multiple_attrs.contains(","));
}
#[test]
fn it_should_display_attr() {
macro_rules! attr_from {
($attr:ident) => {
ImapMboxAttr::$attr.to_string()
};
($custom:literal) => {
ImapMboxAttr::Custom($custom.into()).to_string()
};
}
assert_eq!("NoInferiors", attr_from![NoInferiors]);
assert_eq!("NoSelect", attr_from![NoSelect]);
assert_eq!("Marked", attr_from![Marked]);
assert_eq!("Unmarked", attr_from![Unmarked]);
assert_eq!("CustomAttr", attr_from!["CustomAttr"]);
}
}
impl<'a> From<&'a [RawImapMboxAttr<'a>]> for ImapMboxAttrs {
fn from(raw_attrs: &'a [RawImapMboxAttr<'a>]) -> Self {
Self(raw_attrs.iter().map(ImapMboxAttr::from).collect())
}
}
impl<'a> From<&'a RawImapMboxAttr<'a>> for ImapMboxAttr {
fn from(attr: &'a RawImapMboxAttr<'a>) -> Self {
match attr {
RawImapMboxAttr::NoInferiors => Self::NoInferiors,
RawImapMboxAttr::NoSelect => Self::NoSelect,
RawImapMboxAttr::Marked => Self::Marked,
RawImapMboxAttr::Unmarked => Self::Unmarked,
RawImapMboxAttr::Custom(cow) => Self::Custom(cow.to_string()),
}
}
}
+61
View File
@@ -0,0 +1,61 @@
//! Message sort criteria module.
//!
//! This module regroups everything related to deserialization of
//! message sort criteria.
use anyhow::{anyhow, Error, Result};
use std::{convert::TryFrom, ops::Deref};
/// Represents the message sort criteria. It is just a wrapper around
/// the `imap::extensions::sort::SortCriterion`.
pub struct SortCriteria<'a>(Vec<imap::extensions::sort::SortCriterion<'a>>);
impl<'a> Deref for SortCriteria<'a> {
type Target = Vec<imap::extensions::sort::SortCriterion<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> TryFrom<&'a str> for SortCriteria<'a> {
type Error = Error;
fn try_from(criteria_str: &'a str) -> Result<Self, Self::Error> {
let mut criteria = vec![];
for criterion_str in criteria_str.split(" ") {
criteria.push(match criterion_str.trim() {
"arrival:asc" | "arrival" => Ok(imap::extensions::sort::SortCriterion::Arrival),
"arrival:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Arrival,
)),
"cc:asc" | "cc" => Ok(imap::extensions::sort::SortCriterion::Cc),
"cc:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Cc,
)),
"date:asc" | "date" => Ok(imap::extensions::sort::SortCriterion::Date),
"date:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Date,
)),
"from:asc" | "from" => Ok(imap::extensions::sort::SortCriterion::From),
"from:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::From,
)),
"size:asc" | "size" => Ok(imap::extensions::sort::SortCriterion::Size),
"size:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Size,
)),
"subject:asc" | "subject" => Ok(imap::extensions::sort::SortCriterion::Subject),
"subject:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Subject,
)),
"to:asc" | "to" => Ok(imap::extensions::sort::SortCriterion::To),
"to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::To,
)),
_ => Err(anyhow!("cannot parse sort criterion {:?}", criterion_str)),
}?);
}
Ok(Self(criteria))
}
}
+185
View File
@@ -0,0 +1,185 @@
use anyhow::{anyhow, Context, Result};
use std::{convert::TryInto, fs, path::PathBuf};
use crate::{
backends::{Backend, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
config::{AccountConfig, MaildirBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
};
pub struct MaildirBackend<'a> {
mdir: maildir::Maildir,
account_config: &'a AccountConfig,
}
impl<'a> MaildirBackend<'a> {
pub fn new(
account_config: &'a AccountConfig,
maildir_config: &'a MaildirBackendConfig,
) -> Self {
Self {
account_config,
mdir: maildir_config.maildir_dir.clone().into(),
}
}
fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result<PathBuf> {
if mdir_path.is_dir() {
Ok(mdir_path)
} else {
Err(anyhow!(
"cannot read maildir from directory {:?}",
mdir_path
))
}
}
fn get_mdir_from_name(&self, mdir: &str) -> Result<maildir::Maildir> {
if mdir == self.account_config.inbox_folder {
self.validate_mdir_path(self.mdir.path().to_owned())
.map(maildir::Maildir::from)
} else {
self.validate_mdir_path(mdir.into())
.or_else(|_| {
let path = self.mdir.path().join(format!(".{}", mdir));
self.validate_mdir_path(path)
})
.map(maildir::Maildir::from)
}
}
}
impl<'a> Backend<'a> for MaildirBackend<'a> {
fn add_mbox(&mut self, mdir: &str) -> Result<()> {
fs::create_dir(self.mdir.path().join(format!(".{}", mdir)))
.context(format!("cannot create maildir subfolder {:?}", mdir))
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
let mboxes: MaildirMboxes = self.mdir.list_subdirs().try_into()?;
Ok(Box::new(mboxes))
}
fn del_mbox(&mut self, mdir: &str) -> Result<()> {
fs::remove_dir_all(self.mdir.path().join(format!(".{}", mdir)))
.context(format!("cannot delete maildir subfolder {:?}", mdir))
}
fn get_envelopes(
&mut self,
mdir: &str,
_sort: &str,
filter: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
let mdir = self.get_mdir_from_name(mdir)?;
let mail_entries = match filter {
"new" => mdir.list_new(),
_ => mdir.list_cur(),
};
let mut envelopes: MaildirEnvelopes = mail_entries
.try_into()
.context("cannot parse maildir envelopes from {:?}")?;
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
let page_begin = page * page_size;
if page_begin > envelopes.len() {
return Err(anyhow!(format!(
"cannot list maildir envelopes at page {:?} (out of bounds)",
page_begin + 1,
)));
}
let page_end = envelopes.len().min(page_begin + page_size);
envelopes.0 = envelopes[page_begin..page_end].to_owned();
Ok(Box::new(envelopes))
}
fn add_msg(&mut self, mdir: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags.try_into()?;
let id = mdir
.store_cur_with_flags(msg, &flags.to_string())
.context(format!(
"cannot add message to the \"cur\" folder of maildir {:?}",
mdir.path()
))?;
Ok(Box::new(id))
}
fn get_msg(&mut self, mdir: &str, id: &str) -> Result<Msg> {
let mdir = self.get_mdir_from_name(mdir)?;
let mut mail_entry = mdir
.find(id)
.ok_or_else(|| anyhow!("cannot find maildir message {:?} in {:?}", id, mdir.path()))?;
let parsed_mail = mail_entry.parsed().context(format!(
"cannot parse maildir message {:?} in {:?}",
id,
mdir.path()
))?;
Msg::from_parsed_mail(parsed_mail, self.account_config).context(format!(
"cannot parse maildir message {:?} from {:?}",
id,
mdir.path()
))
}
fn copy_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> {
let mdir_src = self.get_mdir_from_name(mdir_src)?;
let mdir_dst = self.get_mdir_from_name(mdir_dst)?;
mdir_src.copy_to(id, &mdir_dst).context(format!(
"cannot copy message {:?} from maildir {:?} to maildir {:?}",
id,
mdir_src.path(),
mdir_dst.path()
))
}
fn move_msg(&mut self, mdir_src: &str, mdir_dst: &str, id: &str) -> Result<()> {
let mdir_src = self.get_mdir_from_name(mdir_src)?;
let mdir_dst = self.get_mdir_from_name(mdir_dst)?;
mdir_src.move_to(id, &mdir_dst).context(format!(
"cannot move message {:?} from maildir {:?} to maildir {:?}",
id,
mdir_src.path(),
mdir_dst.path()
))
}
fn del_msg(&mut self, mdir: &str, id: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
mdir.delete(id).context(format!(
"cannot delete message {:?} from maildir {:?}",
id,
mdir.path()
))
}
fn add_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags_str.try_into()?;
mdir.add_flags(id, &flags.to_string()).context(format!(
"cannot add flags {:?} to maildir message {:?}",
flags_str, id
))
}
fn set_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags_str.try_into()?;
mdir.set_flags(id, &flags.to_string()).context(format!(
"cannot set flags {:?} to maildir message {:?}",
flags_str, id
))
}
fn del_flags(&mut self, mdir: &str, id: &str, flags_str: &str) -> Result<()> {
let mdir = self.get_mdir_from_name(mdir)?;
let flags: MaildirFlags = flags_str.try_into()?;
mdir.remove_flags(id, &flags.to_string()).context(format!(
"cannot remove flags {:?} from maildir message {:?}",
flags_str, id
))
}
}
+187
View File
@@ -0,0 +1,187 @@
//! Maildir mailbox module.
//!
//! This module provides Maildir types and conversion utilities
//! related to the envelope
use anyhow::{anyhow, Context, Error, Result};
use chrono::DateTime;
use log::{debug, info, trace};
use std::{
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
use crate::{
backends::{MaildirFlag, MaildirFlags},
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(pub Vec<MaildirEnvelope>);
impl Deref for MaildirEnvelopes {
type Target = Vec<MaildirEnvelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for MaildirEnvelopes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl PrintTable for MaildirEnvelopes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
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 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("IDENTIFIER").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 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(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 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;
impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
type Error = Error;
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
info!("begin: try building envelope from maildir parsed mail");
let mut envelope = Self {
id: mail_entry.id().into(),
flags: (&mail_entry)
.try_into()
.context("cannot parse maildir flags")?,
..Self::default()
};
let parsed_mail = mail_entry
.parsed()
.context("cannot parse maildir mail entry")?;
debug!("begin: parse headers");
for h in parsed_mail.get_headers() {
let k = h.get_key();
debug!("header key: {:?}", k);
let v = rfc2047_decoder::decode(h.get_value_raw())
.context(format!("cannot decode value from header {:?}", k))?;
debug!("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"))?;
}
_ => (),
}
}
debug!("end: parse headers");
trace!("envelope: {:?}", envelope);
info!("end: try building envelope from maildir parsed mail");
Ok(envelope)
}
}
+129
View File
@@ -0,0 +1,129 @@
use anyhow::{anyhow, Error, Result};
use std::{
convert::{TryFrom, TryInto},
ops::Deref,
};
/// 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)),
}
}
}
+141
View File
@@ -0,0 +1,141 @@
//! Maildir mailbox module.
//!
//! This module provides Maildir types and conversion utilities
//! related to the mailbox
use anyhow::{anyhow, Error, Result};
use std::{
convert::{TryFrom, TryInto},
ffi::OsStr,
fmt::{self, Display},
ops::Deref,
};
use crate::{
mbox::Mboxes,
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents a list of Maildir mailboxes.
#[derive(Debug, Default, serde::Serialize)]
pub struct MaildirMboxes(pub Vec<MaildirMbox>);
impl Deref for MaildirMboxes {
type Target = Vec<MaildirMbox>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for MaildirMboxes {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?;
Table::print(writter, self, opts)?;
writeln!(writter)?;
Ok(())
}
}
impl Mboxes for MaildirMboxes {
//
}
/// Represents the mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct MaildirMbox {
/// Represents the mailbox name.
pub name: String,
}
impl MaildirMbox {
pub fn new(name: &str) -> Self {
Self { name: name.into() }
}
}
impl Display for MaildirMbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for MaildirMbox {
fn head() -> Row {
Row::new().cell(Cell::new("SUBDIR").bold().underline().white())
}
fn row(&self) -> Row {
Row::new().cell(Cell::new(&self.name).green())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_create_new_mbox() {
assert_eq!(MaildirMbox::default(), MaildirMbox::new(""));
assert_eq!(
MaildirMbox {
name: "INBOX".into(),
..MaildirMbox::default()
},
MaildirMbox::new("INBOX")
);
}
#[test]
fn it_should_display_mbox() {
let default_mbox = MaildirMbox::default();
assert_eq!("", default_mbox.to_string());
let new_mbox = MaildirMbox::new("INBOX");
assert_eq!("INBOX", new_mbox.to_string());
let full_mbox = MaildirMbox {
name: "Sent".into(),
};
assert_eq!("Sent", full_mbox.to_string());
}
}
/// Represents a list of raw mailboxes returned by the `maildir` crate.
pub type RawMaildirMboxes = maildir::MaildirEntries;
impl TryFrom<RawMaildirMboxes> for MaildirMboxes {
type Error = Error;
fn try_from(mail_entries: RawMaildirMboxes) -> Result<Self, Self::Error> {
let mut mboxes = vec![];
for entry in mail_entries {
mboxes.push(entry?.try_into()?);
}
Ok(MaildirMboxes(mboxes))
}
}
/// Represents the raw mailbox returned by the `maildir` crate.
pub type RawMaildirMbox = maildir::Maildir;
impl TryFrom<RawMaildirMbox> for MaildirMbox {
type Error = Error;
fn try_from(mail_entry: RawMaildirMbox) -> Result<Self, Self::Error> {
let subdir_name = mail_entry.path().file_name();
Ok(Self {
name: subdir_name
.and_then(OsStr::to_str)
.and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) })
.ok_or_else(|| {
anyhow!(
"cannot parse maildir subdirectory name from path {:?}",
subdir_name,
)
})?
.into(),
})
}
}