mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 13:17:55 +08:00
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:
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user