clean msg flags, merge tpl entity in msg (#231)

* merge tpl entity into msg

* change envelope subject type to cow

* msg: fix save command when raw msg comes from stdin

* msg: clean flags
This commit is contained in:
Clément DOUIN 2021-10-23 00:17:24 +02:00 committed by GitHub
parent 45aac9fbec
commit d9272917f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 228 additions and 264 deletions

View file

@ -14,7 +14,7 @@ use std::{
use crate::{ use crate::{
config::{Account, Config}, config::{Account, Config},
domain::{Envelopes, Flags, Mbox, Mboxes, Msg, RawMboxes}, domain::{Envelopes, Flags, Mbox, Mboxes, Msg, RawEnvelopes, RawMboxes},
}; };
type ImapSession = imap::Session<TlsStream<TcpStream>>; type ImapSession = imap::Session<TlsStream<TcpStream>>;
@ -23,8 +23,13 @@ pub trait ImapServiceInterface<'a> {
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>; fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>;
fn watch(&mut self, keepalive: u64) -> Result<()>; fn watch(&mut self, keepalive: u64) -> Result<()>;
fn fetch_mboxes(&'a mut self) -> Result<Mboxes>; fn fetch_mboxes(&'a mut self) -> Result<Mboxes>;
fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes>; fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes>;
fn find_msgs(&mut self, query: &str, page_size: &usize, page: &usize) -> Result<Envelopes>; fn fetch_envelopes_with(
&'a mut self,
query: &str,
page_size: &usize,
page: &usize,
) -> Result<Envelopes>;
fn find_msg(&mut self, seq: &str) -> Result<Msg>; fn find_msg(&mut self, seq: &str) -> Result<Msg>;
fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>>; fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>>;
fn append_msg(&mut self, mbox: &Mbox, msg: Msg) -> Result<()>; fn append_msg(&mut self, mbox: &Mbox, msg: Msg) -> Result<()>;
@ -48,6 +53,7 @@ pub struct ImapService<'a> {
/// outside of handlers. Without that, it would be impossible for handlers to return a `Mbox` /// outside of handlers. Without that, it would be impossible for handlers to return a `Mbox`
/// struct or a `Mboxes` struct due to the `ZeroCopy` constraint. /// struct or a `Mboxes` struct due to the `ZeroCopy` constraint.
_raw_mboxes_cache: Option<RawMboxes>, _raw_mboxes_cache: Option<RawMboxes>,
_raw_msgs_cache: Option<RawEnvelopes>,
} }
impl<'a> ImapService<'a> { impl<'a> ImapService<'a> {
@ -115,7 +121,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
Ok(Mboxes::from(self._raw_mboxes_cache.as_ref().unwrap())) Ok(Mboxes::from(self._raw_mboxes_cache.as_ref().unwrap()))
} }
fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes> { fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes> {
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
let last_seq = self let last_seq = self
.sess()? .sess()?
@ -141,11 +147,16 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
.sess()? .sess()?
.fetch(range, "(ENVELOPE FLAGS INTERNALDATE)") .fetch(range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(r#"cannot fetch messages within range "{}""#)?; .context(r#"cannot fetch messages within range "{}""#)?;
self._raw_msgs_cache = Some(fetches);
Ok(Envelopes::try_from(fetches)?) Ok(Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())?)
} }
fn find_msgs(&mut self, query: &str, page_size: &usize, page: &usize) -> Result<Envelopes> { fn fetch_envelopes_with(
&'a mut self,
query: &str,
page_size: &usize,
page: &usize,
) -> Result<Envelopes> {
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
self.sess()? self.sess()?
.select(&mbox.name) .select(&mbox.name)
@ -174,8 +185,8 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
.sess()? .sess()?
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(r#"cannot fetch messages within range "{}""#)?; .context(r#"cannot fetch messages within range "{}""#)?;
self._raw_msgs_cache = Some(fetches);
Ok(Envelopes::try_from(fetches)?) Ok(Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())?)
} }
/// Find a message by sequence number. /// Find a message by sequence number.
@ -388,6 +399,7 @@ impl<'a> From<(&'a Account, &'a Mbox<'a>)> for ImapService<'a> {
mbox, mbox,
sess: None, sess: None,
_raw_mboxes_cache: None, _raw_mboxes_cache: None,
_raw_msgs_cache: None,
} }
} }
} }

View file

@ -2,7 +2,7 @@
//! //!
//! This module contains the definition of the mailbox attribute and its traits implementations. //! This module contains the definition of the mailbox attribute and its traits implementations.
pub(crate) use imap::types::NameAttribute as AttrRemote; pub use imap::types::NameAttribute as AttrRemote;
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
borrow::Cow, borrow::Cow,

View file

@ -9,13 +9,13 @@ use log::trace;
/// Represents the mailbox commands. /// Represents the mailbox commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub(crate) enum Cmd { pub enum Cmd {
/// Represents the list mailboxes command. /// Represents the list mailboxes command.
List, List,
} }
/// Defines the mailbox command matcher. /// Defines the mailbox command matcher.
pub(crate) fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> { pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
if let Some(_) = m.subcommand_matches("mailboxes") { if let Some(_) = m.subcommand_matches("mailboxes") {
trace!("mailboxes subcommand matched"); trace!("mailboxes subcommand matched");
return Ok(Some(Cmd::List)); return Ok(Some(Cmd::List));
@ -25,14 +25,14 @@ pub(crate) fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
} }
/// Contains mailbox subcommands. /// Contains mailbox subcommands.
pub(crate) fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
vec![clap::SubCommand::with_name("mailboxes") vec![clap::SubCommand::with_name("mailboxes")
.aliases(&["mailbox", "mboxes", "mbox", "mb", "m"]) .aliases(&["mailbox", "mboxes", "mbox", "mb", "m"])
.about("Lists mailboxes")] .about("Lists mailboxes")]
} }
/// Defines the source mailbox argument. /// Defines the source mailbox argument.
pub(crate) fn source_arg<'a>() -> clap::Arg<'a, 'a> { pub fn source_arg<'a>() -> clap::Arg<'a, 'a> {
clap::Arg::with_name("mbox-source") clap::Arg::with_name("mbox-source")
.short("m") .short("m")
.long("mailbox") .long("mailbox")
@ -42,7 +42,7 @@ pub(crate) fn source_arg<'a>() -> clap::Arg<'a, 'a> {
} }
/// Defines the target mailbox argument. /// Defines the target mailbox argument.
pub(crate) fn target_arg<'a>() -> clap::Arg<'a, 'a> { pub fn target_arg<'a>() -> clap::Arg<'a, 'a> {
clap::Arg::with_name("mbox-target") clap::Arg::with_name("mbox-target")
.help("Specifies the targetted mailbox") .help("Specifies the targetted mailbox")
.value_name("TARGET") .value_name("TARGET")

View file

@ -14,7 +14,7 @@ use crate::{
}; };
/// Represents a raw mailbox returned by the `imap` crate. /// Represents a raw mailbox returned by the `imap` crate.
pub(crate) type RawMbox = imap::types::Name; pub type RawMbox = imap::types::Name;
/// Represents a mailbox. /// Represents a mailbox.
#[derive(Debug, Default, PartialEq, Eq, Serialize)] #[derive(Debug, Default, PartialEq, Eq, Serialize)]

View file

@ -77,11 +77,11 @@ mod tests {
unimplemented!() unimplemented!()
} }
fn get_msgs(&mut self, _: &usize, _: &usize) -> Result<Envelopes> { fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {
unimplemented!() unimplemented!()
} }
fn find_msgs(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> { fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
unimplemented!() unimplemented!()
} }

View file

@ -1,16 +1,18 @@
use anyhow::{anyhow, Context, Error, Result}; use anyhow::{anyhow, Context, Error, Result};
use serde::Serialize; use serde::Serialize;
use std::convert::TryFrom; use std::{borrow::Cow, convert::TryFrom};
use crate::{ use crate::{
domain::msg::{Flag, Flags}, domain::msg::{Flag, Flags},
ui::table::{Cell, Row, Table}, ui::table::{Cell, Row, Table},
}; };
pub type RawEnvelope = imap::types::Fetch;
/// Representation of an envelope. An envelope gathers basic information related to a message. It /// Representation of an envelope. An envelope gathers basic information related to a message. It
/// is mostly used for listings. /// is mostly used for listings.
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct Envelope { pub struct Envelope<'a> {
/// The sequence number of the message. /// The sequence number of the message.
/// ///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2 /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
@ -20,7 +22,7 @@ pub struct Envelope {
pub flags: Flags, pub flags: Flags,
/// The subject of the message. /// The subject of the message.
pub subject: String, pub subject: Cow<'a, str>,
/// The sender of the message. /// The sender of the message.
pub sender: String, pub sender: String,
@ -31,10 +33,10 @@ pub struct Envelope {
pub date: Option<String>, pub date: Option<String>,
} }
impl<'a> TryFrom<&'a imap::types::Fetch> for Envelope { impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
type Error = Error; type Error = Error;
fn try_from(fetch: &'a imap::types::Fetch) -> Result<Envelope> { fn try_from(fetch: &'a RawEnvelope) -> Result<Envelope> {
let envelope = fetch let envelope = fetch
.envelope() .envelope()
.ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?; .ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?;
@ -46,7 +48,7 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Envelope {
let flags = Flags::try_from(fetch.flags())?; let flags = Flags::try_from(fetch.flags())?;
// Get the subject // Get the subject
let subject = envelope let subject: Cow<str> = envelope
.subject .subject
.as_ref() .as_ref()
.ok_or(anyhow!("cannot get subject of message {}", fetch.message)) .ok_or(anyhow!("cannot get subject of message {}", fetch.message))
@ -55,7 +57,8 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Envelope {
"cannot decode subject of message {}", "cannot decode subject of message {}",
fetch.message fetch.message
)) ))
})?; })?
.into();
// Get the sender // Get the sender
let sender = envelope let sender = envelope
@ -114,7 +117,7 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Envelope {
} }
} }
impl Table for Envelope { impl<'a> Table for Envelope<'a> {
fn head() -> Row { fn head() -> Row {
Row::new() Row::new()
.cell(Cell::new("ID").bold().underline().white()) .cell(Cell::new("ID").bold().underline().white())

View file

@ -1,5 +1,4 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use imap::types::{Fetch, ZeroCopy};
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
convert::TryFrom, convert::TryFrom,
@ -7,24 +6,29 @@ use std::{
ops::Deref, ops::Deref,
}; };
use crate::{domain::msg::Envelope, ui::Table}; use crate::{
domain::{msg::Envelope, RawEnvelope},
ui::Table,
};
pub type RawEnvelopes = imap::types::ZeroCopy<Vec<RawEnvelope>>;
/// Representation of a list of envelopes. /// Representation of a list of envelopes.
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Serialize)]
pub struct Envelopes(pub Vec<Envelope>); pub struct Envelopes<'a>(pub Vec<Envelope<'a>>);
impl Deref for Envelopes { impl<'a> Deref for Envelopes<'a> {
type Target = Vec<Envelope>; type Target = Vec<Envelope<'a>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
} }
} }
impl TryFrom<ZeroCopy<Vec<Fetch>>> for Envelopes { impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> {
type Error = Error; type Error = Error;
fn try_from(fetches: ZeroCopy<Vec<Fetch>>) -> Result<Self> { fn try_from(fetches: &'a RawEnvelopes) -> Result<Self> {
let mut envelopes = vec![]; let mut envelopes = vec![];
for fetch in fetches.iter().rev() { for fetch in fetches.iter().rev() {
@ -35,7 +39,7 @@ impl TryFrom<ZeroCopy<Vec<Fetch>>> for Envelopes {
} }
} }
impl Display for Envelopes { impl<'a> Display for Envelopes<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "\n{}", Table::render(&self)) writeln!(f, "\n{}", Table::render(&self))
} }

View file

@ -1,6 +1,7 @@
//! Module related to message flag CLI. //! Message flag CLI module.
//! //!
//! This module provides subcommands, arguments and a command matcher related to message flag. //! This module provides subcommands, arguments and a command matcher related to the message flag
//! domain.
use anyhow::Result; use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
@ -11,37 +12,40 @@ use crate::domain::msg::msg_arg;
type SeqRange<'a> = &'a str; type SeqRange<'a> = &'a str;
type Flags<'a> = Vec<&'a str>; type Flags<'a> = Vec<&'a str>;
/// Message flag commands. /// Represents the flag commands.
pub enum Command<'a> { pub enum Command<'a> {
Set(SeqRange<'a>, Flags<'a>), /// Represents the add flags command.
Add(SeqRange<'a>, Flags<'a>), Add(SeqRange<'a>, Flags<'a>),
/// Represents the set flags command.
Set(SeqRange<'a>, Flags<'a>),
/// Represents the remove flags command.
Remove(SeqRange<'a>, Flags<'a>), Remove(SeqRange<'a>, Flags<'a>),
} }
/// Message flag command matcher. /// Defines the flag command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> { pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("add") { if let Some(m) = m.subcommand_matches("add") {
debug!("add command matched"); debug!("add subcommand matched");
let seq_range = m.value_of("seq-range").unwrap(); let seq_range = m.value_of("seq-range").unwrap();
trace!(r#"seq range: "{:?}""#, seq_range); trace!(r#"seq range: "{}""#, seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
trace!(r#"flags: "{:?}""#, flags); trace!(r#"flags: "{:?}""#, flags);
return Ok(Some(Command::Add(seq_range, flags))); return Ok(Some(Command::Add(seq_range, flags)));
} }
if let Some(m) = m.subcommand_matches("set") { if let Some(m) = m.subcommand_matches("set") {
debug!("set command matched"); debug!("set subcommand matched");
let seq_range = m.value_of("seq-range").unwrap(); let seq_range = m.value_of("seq-range").unwrap();
trace!(r#"seq range: "{:?}""#, seq_range); trace!(r#"seq range: "{}""#, seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
trace!(r#"flags: "{:?}""#, flags); trace!(r#"flags: "{:?}""#, flags);
return Ok(Some(Command::Set(seq_range, flags))); return Ok(Some(Command::Set(seq_range, flags)));
} }
if let Some(m) = m.subcommand_matches("remove") { if let Some(m) = m.subcommand_matches("remove") {
debug!("remove command matched"); trace!("remove subcommand matched");
let seq_range = m.value_of("seq-range").unwrap(); let seq_range = m.value_of("seq-range").unwrap();
trace!(r#"seq range: "{:?}""#, seq_range); trace!(r#"seq range: "{}""#, seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
trace!(r#"flags: "{:?}""#, flags); trace!(r#"flags: "{:?}""#, flags);
return Ok(Some(Command::Remove(seq_range, flags))); return Ok(Some(Command::Remove(seq_range, flags)));
@ -50,7 +54,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
Ok(None) Ok(None)
} }
/// Message flag flags argument. /// Defines the flags argument.
fn flags_arg<'a>() -> Arg<'a, 'a> { fn flags_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("flags") Arg::with_name("flags")
.help("IMAP flags") .help("IMAP flags")
@ -60,7 +64,7 @@ fn flags_arg<'a>() -> Arg<'a, 'a> {
.required(true) .required(true)
} }
/// Message flag subcommands. /// Contains flag subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("flag") vec![SubCommand::with_name("flag")
.aliases(&["flags", "flg"]) .aliases(&["flags", "flg"])
@ -68,19 +72,21 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.setting(AppSettings::SubcommandRequiredElseHelp) .setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand( .subcommand(
SubCommand::with_name("add") SubCommand::with_name("add")
.aliases(&["a"])
.about("Adds flags to a message") .about("Adds flags to a message")
.arg(msg_arg::seq_range_arg()) .arg(msg_arg::seq_range_arg())
.arg(flags_arg()), .arg(flags_arg()),
) )
.subcommand( .subcommand(
SubCommand::with_name("set") SubCommand::with_name("set")
.aliases(&["s", "change", "c"])
.about("Replaces all message flags") .about("Replaces all message flags")
.arg(msg_arg::seq_range_arg()) .arg(msg_arg::seq_range_arg())
.arg(flags_arg()), .arg(flags_arg()),
) )
.subcommand( .subcommand(
SubCommand::with_name("remove") SubCommand::with_name("remove")
.aliases(&["rm"]) .aliases(&["rem", "rm", "r", "delete", "del", "d"])
.about("Removes flags from a message") .about("Removes flags from a message")
.arg(msg_arg::seq_range_arg()) .arg(msg_arg::seq_range_arg())
.arg(flags_arg()), .arg(flags_arg()),

View file

@ -1,10 +1,15 @@
pub use imap::types::Flag; pub use imap::types::Flag;
use serde::ser::{Serialize, Serializer}; use serde::ser::{Serialize, Serializer};
/// Serializable wrapper arround [`imap::types::Flag`]. /// Represents a serializable `imap::types::Flag`.
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct SerializableFlag<'a>(pub &'a Flag<'a>); pub struct SerializableFlag<'a>(pub &'a Flag<'a>);
/// Implements the serialize trait for `imap::types::Flag`.
/// Remote serialization cannot be used because of the [#[non_exhaustive]] directive of
/// `imap::types::Flag`.
///
/// [#[non_exhaustive]]: https://github.com/serde-rs/serde/issues/1991
impl<'a> Serialize for SerializableFlag<'a> { impl<'a> Serialize for SerializableFlag<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where

View file

@ -1,15 +1,15 @@
//! Module related to message flag handling. //! Message flag handling module.
//! //!
//! This module gathers all message flag commands. //! This module gathers all flag actions triggered by the CLI.
use anyhow::Result; use anyhow::Result;
use crate::{ use crate::{
domain::{imap::ImapServiceInterface, msg::Flags}, domain::{Flags, ImapServiceInterface},
output::OutputServiceInterface, output::OutputServiceInterface,
}; };
/// Add flags to all messages within the given sequence range. /// Adds flags to all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`. /// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str, seq_range: &'a str,
@ -25,7 +25,7 @@ pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceIn
)) ))
} }
/// Remove flags from all messages within the given sequence range. /// Removes flags from all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`. /// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str, seq_range: &'a str,
@ -41,7 +41,7 @@ pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServic
)) ))
} }
/// Replace flags of all messages within the given sequence range. /// Replaces flags of all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`. /// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
seq_range: &'a str, seq_range: &'a str,

View file

@ -10,12 +10,13 @@ use std::{
use crate::domain::msg::{Flag, SerializableFlag}; use crate::domain::msg::{Flag, SerializableFlag};
/// Wrapper arround [`imap::types::Flag`]s. /// Represents the flags of the message.
/// A hashset is used to avoid duplicates.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Flags(pub HashSet<Flag<'static>>); pub struct Flags(pub HashSet<Flag<'static>>);
impl Flags { impl Flags {
/// Build a symbols string based on flags contained in the hashset. /// Builds a symbols string based on flags contained in the hashset.
pub fn to_symbols_string(&self) -> String { pub fn to_symbols_string(&self) -> String {
let mut flags = String::new(); let mut flags = String::new();
flags.push_str(if self.contains(&Flag::Seen) { flags.push_str(if self.contains(&Flag::Seen) {
@ -120,48 +121,6 @@ impl Serialize for Flags {
} }
} }
///// Converst a string of flags into their appropriate flag representation. For example `"Seen"` is
///// gonna be convertred to `Flag::Seen`.
/////
///// # Example
///// ```rust
///// use himalaya::flag::model::Flags;
///// use imap::types::Flag;
///// use std::collections::HashSet;
/////
///// fn main() {
///// let flags = "Seen Answered";
/////
///// let mut expected = HashSet::new();
///// expected.insert(Flag::Seen);
///// expected.insert(Flag::Answered);
/////
///// let output = Flags::from(flags);
/////
///// assert_eq!(output.0, expected);
///// }
///// ```
//impl From<&str> for Flags {
// fn from(flags: &str) -> Self {
// let mut content: HashSet<Flag<'static>> = HashSet::new();
// for flag in flags.split_ascii_whitespace() {
// match flag {
// "Answered" => content.insert(Flag::Answered),
// "Deleted" => content.insert(Flag::Deleted),
// "Draft" => content.insert(Flag::Draft),
// "Flagged" => content.insert(Flag::Flagged),
// "MayCreate" => content.insert(Flag::MayCreate),
// "Recent" => content.insert(Flag::Recent),
// "Seen" => content.insert(Flag::Seen),
// custom => content.insert(Flag::Custom(Cow::Owned(custom.to_string()))),
// };
// }
// Self(content)
// }
//}
impl<'a> From<Vec<&'a str>> for Flags { impl<'a> From<Vec<&'a str>> for Flags {
fn from(flags: Vec<&'a str>) -> Self { fn from(flags: Vec<&'a str>) -> Self {
let mut map: HashSet<Flag<'static>> = HashSet::new(); let mut map: HashSet<Flag<'static>> = HashSet::new();
@ -185,6 +144,7 @@ impl<'a> From<Vec<&'a str>> for Flags {
} }
} }
// FIXME
//#[cfg(test)] //#[cfg(test)]
//mod tests { //mod tests {
// use crate::domain::msg::flag::entity::Flags; // use crate::domain::msg::flag::entity::Flags;

View file

@ -43,9 +43,6 @@ pub use tpl_arg::TplOverride;
pub mod tpl_handler; pub mod tpl_handler;
pub mod tpl_entity;
pub use tpl_entity::*;
pub mod msg_entity; pub mod msg_entity;
pub use msg_entity::*; pub use msg_entity::*;

View file

@ -32,7 +32,7 @@ pub enum Command<'a> {
Move(Seq<'a>, Mbox<'a>), Move(Seq<'a>, Mbox<'a>),
Read(Seq<'a>, TextMime<'a>, Raw), Read(Seq<'a>, TextMime<'a>, Raw),
Reply(Seq<'a>, All, AttachmentsPaths<'a>), Reply(Seq<'a>, All, AttachmentsPaths<'a>),
Save(Mbox<'a>, RawMsg<'a>), Save(RawMsg<'a>),
Search(Query, Option<PageSize>, Page), Search(Query, Option<PageSize>, Page),
Send(RawMsg<'a>), Send(RawMsg<'a>),
Write(AttachmentsPaths<'a>), Write(AttachmentsPaths<'a>),
@ -123,11 +123,9 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("save") { if let Some(m) = m.subcommand_matches("save") {
debug!("save command matched"); debug!("save command matched");
let msg = m.value_of("message").unwrap(); let msg = m.value_of("message").unwrap_or_default();
debug!("message: {}", &msg); trace!("message: {}", msg);
let mbox = m.value_of("mbox-target").unwrap(); return Ok(Some(Command::Save(msg)));
debug!("target mailbox: `{:?}`", mbox);
return Ok(Some(Command::Save(mbox, msg)));
} }
if let Some(m) = m.subcommand_matches("search") { if let Some(m) = m.subcommand_matches("search") {
@ -197,7 +195,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
} }
/// Message sequence number argument. /// Message sequence number argument.
pub(crate) fn seq_arg<'a>() -> Arg<'a, 'a> { pub fn seq_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("seq") Arg::with_name("seq")
.help("Specifies the targetted message") .help("Specifies the targetted message")
.value_name("SEQ") .value_name("SEQ")
@ -205,7 +203,7 @@ pub(crate) fn seq_arg<'a>() -> Arg<'a, 'a> {
} }
/// Message sequence range argument. /// Message sequence range argument.
pub(crate) fn seq_range_arg<'a>() -> Arg<'a, 'a> { pub fn seq_range_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("seq-range") Arg::with_name("seq-range")
.help("Specifies targetted message(s)") .help("Specifies targetted message(s)")
.long_help("Specifies a range of targetted messages. The range follows the [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501#section-9) format: `1:5` matches messages with sequence number between 1 and 5, `1,5` matches messages with sequence number 1 or 5, * matches all messages.") .long_help("Specifies a range of targetted messages. The range follows the [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501#section-9) format: `1:5` matches messages with sequence number between 1 and 5, `1,5` matches messages with sequence number 1 or 5, * matches all messages.")
@ -214,7 +212,7 @@ pub(crate) fn seq_range_arg<'a>() -> Arg<'a, 'a> {
} }
/// Message reply all argument. /// Message reply all argument.
pub(crate) fn reply_all_arg<'a>() -> Arg<'a, 'a> { pub fn reply_all_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("reply-all") Arg::with_name("reply-all")
.help("Includes all recipients") .help("Includes all recipients")
.short("A") .short("A")

View file

@ -4,6 +4,7 @@ use chrono::{DateTime, FixedOffset};
use html_escape; use html_escape;
use imap::types::Flag; use imap::types::Flag;
use lettre::message::{Attachment, MultiPart, SinglePart}; use lettre::message::{Attachment, MultiPart, SinglePart};
use log::trace;
use regex::Regex; use regex::Regex;
use rfc2047_decoder; use rfc2047_decoder;
use std::{ use std::{
@ -18,7 +19,7 @@ use crate::{
domain::{ domain::{
imap::ImapServiceInterface, imap::ImapServiceInterface,
mbox::Mbox, mbox::Mbox,
msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, Tpl, TplOverride}, msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, TplOverride},
smtp::SmtpServiceInterface, smtp::SmtpServiceInterface,
}, },
output::OutputServiceInterface, output::OutputServiceInterface,
@ -290,9 +291,9 @@ impl Msg {
} }
fn _edit_with_editor(&self, account: &Account) -> Result<Self> { fn _edit_with_editor(&self, account: &Account) -> Result<Self> {
let tpl = Tpl::from_msg(TplOverride::default(), self, account); let tpl = self.to_tpl(TplOverride::default(), account);
let tpl = editor::open_with_tpl(tpl)?; let tpl = editor::open_with_tpl(tpl)?;
Self::try_from(&tpl) Self::from_tpl(&tpl)
} }
pub fn edit_with_editor< pub fn edit_with_editor<
@ -314,7 +315,7 @@ impl Msg {
Ok(choice) => match choice { Ok(choice) => match choice {
PreEditChoice::Edit => { PreEditChoice::Edit => {
let tpl = editor::open_with_draft()?; let tpl = editor::open_with_draft()?;
self.merge_with(Msg::try_from(&tpl)?); self.merge_with(Msg::from_tpl(&tpl)?);
break; break;
} }
PreEditChoice::Discard => { PreEditChoice::Discard => {
@ -355,7 +356,7 @@ impl Msg {
Ok(PostEditChoice::RemoteDraft) => { Ok(PostEditChoice::RemoteDraft) => {
let mbox = Mbox::new("Drafts"); let mbox = Mbox::new("Drafts");
let flags = Flags::try_from(vec![Flag::Seen, Flag::Draft])?; let flags = Flags::try_from(vec![Flag::Seen, Flag::Draft])?;
let tpl = Tpl::from_msg(TplOverride::default(), &self, account); let tpl = self.to_tpl(TplOverride::default(), account);
imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?; imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?;
msg_utils::remove_local_draft()?; msg_utils::remove_local_draft()?;
output.print("Message successfully saved to Drafts")?; output.print("Message successfully saved to Drafts")?;
@ -439,12 +440,95 @@ impl Msg {
} }
} }
} }
}
impl TryFrom<&Tpl> for Msg { pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> String {
type Error = Error; let mut tpl = String::default();
fn try_from(tpl: &Tpl) -> Result<Msg> { tpl.push_str("Content-Type: text/plain; charset=utf-8\n");
if let Some(in_reply_to) = self.in_reply_to.as_ref() {
tpl.push_str(&format!("In-Reply-To: {}\n", in_reply_to))
}
// From
tpl.push_str(&format!(
"From: {}\n",
opts.from
.map(|addrs| addrs.join(", "))
.unwrap_or_else(|| account.address())
));
// To
tpl.push_str(&format!(
"To: {}\n",
opts.to
.map(|addrs| addrs.join(", "))
.or_else(|| self.to.clone().map(|addrs| addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")))
.unwrap_or_default()
));
// Cc
if let Some(addrs) = opts.cc.map(|addrs| addrs.join(", ")).or_else(|| {
self.cc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
tpl.push_str(&format!("Cc: {}\n", addrs));
}
// Bcc
if let Some(addrs) = opts.bcc.map(|addrs| addrs.join(", ")).or_else(|| {
self.bcc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
tpl.push_str(&format!("Bcc: {}\n", addrs));
}
// Subject
tpl.push_str(&format!(
"Subject: {}\n",
opts.subject.unwrap_or(&self.subject)
));
// Headers <=> body separator
tpl.push_str("\n");
// Body
if let Some(body) = opts.body {
tpl.push_str(body);
} else {
tpl.push_str(&self.fold_text_plain_parts())
}
// Signature
if let Some(sig) = opts.sig {
tpl.push_str("\n\n");
tpl.push_str(sig);
} else if let Some(ref sig) = account.sig {
tpl.push_str("\n\n");
tpl.push_str(sig);
}
tpl.push_str("\n");
trace!("template: {:#?}", tpl);
tpl
}
pub fn from_tpl(tpl: &str) -> Result<Self> {
let mut msg = Msg::default(); let mut msg = Msg::default();
let parsed_msg = let parsed_msg =

View file

@ -19,7 +19,7 @@ use crate::{
domain::{ domain::{
imap::ImapServiceInterface, imap::ImapServiceInterface,
mbox::Mbox, mbox::Mbox,
msg::{Flags, Msg, Part, TextPlainPart, Tpl}, msg::{Flags, Msg, Part, TextPlainPart},
smtp::SmtpServiceInterface, smtp::SmtpServiceInterface,
}, },
output::OutputServiceInterface, output::OutputServiceInterface,
@ -116,7 +116,7 @@ pub fn list<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceI
let page_size = page_size.unwrap_or(account.default_page_size); let page_size = page_size.unwrap_or(account.default_page_size);
trace!("page size: {}", page_size); trace!("page size: {}", page_size);
let msgs = imap.get_msgs(&page_size, &page)?; let msgs = imap.fetch_envelopes(&page_size, &page)?;
trace!("messages: {:#?}", msgs); trace!("messages: {:#?}", msgs);
output.print(msgs) output.print(msgs)
} }
@ -244,14 +244,26 @@ pub fn reply<
} }
/// Save a raw message to the targetted mailbox. /// Save a raw message to the targetted mailbox.
pub fn save<'a, ImapService: ImapServiceInterface<'a>>( pub fn save<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>(
mbox: &str, mbox: &Mbox,
msg: &str, raw_msg: &str,
output: &OutputService,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let mbox = Mbox::new(mbox); let raw_msg = if atty::is(Stream::Stdin) || output.is_json() {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(|ln| ln.ok())
.map(|ln| ln.to_string())
.collect::<Vec<String>>()
.join("\r\n")
};
let flags = Flags::try_from(vec![Flag::Seen])?; let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, msg.as_bytes(), flags) imap.append_raw_msg_with_flags(mbox, raw_msg.as_bytes(), flags)
} }
/// Paginate messages from the selected mailbox matching the specified query. /// Paginate messages from the selected mailbox matching the specified query.
@ -261,12 +273,12 @@ pub fn search<'a, OutputService: OutputServiceInterface, ImapService: ImapServic
page: usize, page: usize,
account: &Account, account: &Account,
output: &OutputService, output: &OutputService,
imap: &mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size); let page_size = page_size.unwrap_or(account.default_page_size);
trace!("page size: {}", page_size); trace!("page size: {}", page_size);
let msgs = imap.find_msgs(&query, &page_size, &page)?; let msgs = imap.fetch_envelopes_with(&query, &page_size, &page)?;
trace!("messages: {:#?}", msgs); trace!("messages: {:#?}", msgs);
output.print(msgs) output.print(msgs)
} }
@ -295,8 +307,7 @@ pub fn send<
.join("\r\n") .join("\r\n")
}; };
let tpl = Tpl(raw_msg.to_string()); let msg = Msg::from_tpl(&raw_msg.to_string())?;
let msg = Msg::try_from(&tpl)?;
let envelope: lettre::address::Envelope = msg.try_into()?; let envelope: lettre::address::Envelope = msg.try_into()?;
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?; smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
debug!("message sent!"); debug!("message sent!");

View file

@ -1,118 +0,0 @@
use log::trace;
use serde::Serialize;
use std::{
fmt::{self, Display},
ops::Deref,
};
use crate::{
config::Account,
domain::msg::{Msg, TplOverride},
};
#[derive(Debug, Default, Clone, Serialize)]
pub struct Tpl(pub String);
impl Tpl {
pub fn from_msg(opts: TplOverride, msg: &Msg, account: &Account) -> Tpl {
let mut tpl = String::default();
tpl.push_str("Content-Type: text/plain; charset=utf-8\n");
if let Some(in_reply_to) = msg.in_reply_to.as_ref() {
tpl.push_str(&format!("In-Reply-To: {}\n", in_reply_to))
}
// From
tpl.push_str(&format!(
"From: {}\n",
opts.from
.map(|addrs| addrs.join(", "))
.unwrap_or_else(|| account.address())
));
// To
tpl.push_str(&format!(
"To: {}\n",
opts.to
.map(|addrs| addrs.join(", "))
.or_else(|| msg.to.clone().map(|addrs| addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")))
.unwrap_or_default()
));
// Cc
if let Some(addrs) = opts.cc.map(|addrs| addrs.join(", ")).or_else(|| {
msg.cc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
tpl.push_str(&format!("Cc: {}\n", addrs));
}
// Bcc
if let Some(addrs) = opts.bcc.map(|addrs| addrs.join(", ")).or_else(|| {
msg.bcc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
tpl.push_str(&format!("Bcc: {}\n", addrs));
}
// Subject
tpl.push_str(&format!(
"Subject: {}\n",
opts.subject.unwrap_or(&msg.subject)
));
// Headers <=> body separator
tpl.push_str("\n");
// Body
if let Some(body) = opts.body {
tpl.push_str(body);
} else {
tpl.push_str(&msg.fold_text_plain_parts())
}
// Signature
if let Some(sig) = opts.sig {
tpl.push_str("\n\n");
tpl.push_str(sig);
} else if let Some(ref sig) = account.sig {
tpl.push_str("\n\n");
tpl.push_str(sig);
}
tpl.push_str("\n");
let tpl = Tpl(tpl);
trace!("template: {:#?}", tpl);
tpl
}
}
impl Deref for Tpl {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Tpl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.deref())
}
}

View file

@ -8,7 +8,7 @@ use crate::{
config::Account, config::Account,
domain::{ domain::{
imap::ImapServiceInterface, imap::ImapServiceInterface,
msg::{Msg, Tpl, TplOverride}, msg::{Msg, TplOverride},
}, },
output::OutputServiceInterface, output::OutputServiceInterface,
}; };
@ -19,8 +19,7 @@ pub fn new<'a, OutputService: OutputServiceInterface>(
account: &'a Account, account: &'a Account,
output: &'a OutputService, output: &'a OutputService,
) -> Result<()> { ) -> Result<()> {
let msg = Msg::default(); let tpl = Msg::default().to_tpl(opts, account);
let tpl = Tpl::from_msg(opts, &msg, account);
output.print(tpl) output.print(tpl)
} }
@ -33,8 +32,10 @@ pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapService
output: &'a OutputService, output: &'a OutputService,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let msg = imap.find_msg(seq)?.into_reply(all, account)?; let tpl = imap
let tpl = Tpl::from_msg(opts, &msg, account); .find_msg(seq)?
.into_reply(all, account)?
.to_tpl(opts, account);
output.print(tpl) output.print(tpl)
} }
@ -46,7 +47,9 @@ pub fn forward<'a, OutputService: OutputServiceInterface, ImapService: ImapServi
output: &'a OutputService, output: &'a OutputService,
imap: &'a mut ImapService, imap: &'a mut ImapService,
) -> Result<()> { ) -> Result<()> {
let msg = imap.find_msg(seq)?.into_forward(account)?; let tpl = imap
let tpl = Tpl::from_msg(opts, &msg, account); .find_msg(seq)?
.into_forward(account)?
.to_tpl(opts, account);
output.print(tpl) output.print(tpl)
} }

View file

@ -119,8 +119,8 @@ fn main() -> Result<()> {
Some(msg_arg::Command::Reply(seq, all, atts)) => { Some(msg_arg::Command::Reply(seq, all, atts)) => {
return msg_handler::reply(seq, all, atts, &account, &output, &mut imap, &mut smtp); return msg_handler::reply(seq, all, atts, &account, &output, &mut imap, &mut smtp);
} }
Some(msg_arg::Command::Save(mbox, msg)) => { Some(msg_arg::Command::Save(raw_msg)) => {
return msg_handler::save(mbox, msg, &mut imap); return msg_handler::save(&mbox, raw_msg, &output, &mut imap);
} }
Some(msg_arg::Command::Search(query, page_size, page)) => { Some(msg_arg::Command::Search(query, page_size, page)) => {
return msg_handler::search(query, page_size, page, &account, &output, &mut imap); return msg_handler::search(query, page_size, page, &account, &output, &mut imap);

View file

@ -2,9 +2,9 @@ use anyhow::{Context, Result};
use log::debug; use log::debug;
use std::{env, fs, process::Command}; use std::{env, fs, process::Command};
use crate::domain::msg::{msg_utils, Tpl}; use crate::domain::msg::msg_utils;
pub fn open_with_tpl(tpl: Tpl) -> Result<Tpl> { pub fn open_with_tpl(tpl: String) -> Result<String> {
let path = msg_utils::local_draft_path(); let path = msg_utils::local_draft_path();
debug!("create draft"); debug!("create draft");
@ -20,13 +20,12 @@ pub fn open_with_tpl(tpl: Tpl) -> Result<Tpl> {
let content = let content =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
Ok(Tpl(content)) Ok(content)
} }
pub fn open_with_draft() -> Result<Tpl> { pub fn open_with_draft() -> Result<String> {
let path = msg_utils::local_draft_path(); let path = msg_utils::local_draft_path();
let content = let tpl =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
let tpl = Tpl(content);
open_with_tpl(tpl) open_with_tpl(tpl)
} }