From d9272917f5d1241511751cdac77eedd64ec3e5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Sat, 23 Oct 2021 00:17:24 +0200 Subject: [PATCH] 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 --- src/domain/imap/imap_service.rs | 30 +++++--- src/domain/mbox/attr_entity.rs | 2 +- src/domain/mbox/mbox_arg.rs | 10 +-- src/domain/mbox/mbox_entity.rs | 2 +- src/domain/mbox/mbox_handler.rs | 4 +- src/domain/msg/envelope_entity.rs | 19 +++-- src/domain/msg/envelopes_entity.rs | 20 +++-- src/domain/msg/flag_arg.rs | 34 +++++---- src/domain/msg/flag_entity.rs | 7 +- src/domain/msg/flag_handler.rs | 12 +-- src/domain/msg/flags_entity.rs | 48 +----------- src/domain/msg/mod.rs | 3 - src/domain/msg/msg_arg.rs | 16 ++-- src/domain/msg/msg_entity.rs | 102 ++++++++++++++++++++++--- src/domain/msg/msg_handler.rs | 33 +++++--- src/domain/msg/tpl_entity.rs | 118 ----------------------------- src/domain/msg/tpl_handler.rs | 17 +++-- src/main.rs | 4 +- src/ui/editor.rs | 11 ++- 19 files changed, 228 insertions(+), 264 deletions(-) delete mode 100644 src/domain/msg/tpl_entity.rs diff --git a/src/domain/imap/imap_service.rs b/src/domain/imap/imap_service.rs index 5d93721..7db842a 100644 --- a/src/domain/imap/imap_service.rs +++ b/src/domain/imap/imap_service.rs @@ -14,7 +14,7 @@ use std::{ use crate::{ config::{Account, Config}, - domain::{Envelopes, Flags, Mbox, Mboxes, Msg, RawMboxes}, + domain::{Envelopes, Flags, Mbox, Mboxes, Msg, RawEnvelopes, RawMboxes}, }; type ImapSession = imap::Session>; @@ -23,8 +23,13 @@ pub trait ImapServiceInterface<'a> { fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>; fn watch(&mut self, keepalive: u64) -> Result<()>; fn fetch_mboxes(&'a mut self) -> Result; - fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result; - fn find_msgs(&mut self, query: &str, page_size: &usize, page: &usize) -> Result; + fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result; + fn fetch_envelopes_with( + &'a mut self, + query: &str, + page_size: &usize, + page: &usize, + ) -> Result; fn find_msg(&mut self, seq: &str) -> Result; fn find_raw_msg(&mut self, seq: &str) -> 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` /// struct or a `Mboxes` struct due to the `ZeroCopy` constraint. _raw_mboxes_cache: Option, + _raw_msgs_cache: Option, } 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())) } - fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result { + fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result { let mbox = self.mbox.to_owned(); let last_seq = self .sess()? @@ -141,11 +147,16 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { .sess()? .fetch(range, "(ENVELOPE FLAGS INTERNALDATE)") .context(r#"cannot fetch messages within range "{}""#)?; - - Ok(Envelopes::try_from(fetches)?) + self._raw_msgs_cache = Some(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 { + fn fetch_envelopes_with( + &'a mut self, + query: &str, + page_size: &usize, + page: &usize, + ) -> Result { let mbox = self.mbox.to_owned(); self.sess()? .select(&mbox.name) @@ -174,8 +185,8 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { .sess()? .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .context(r#"cannot fetch messages within range "{}""#)?; - - Ok(Envelopes::try_from(fetches)?) + self._raw_msgs_cache = Some(fetches); + Ok(Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())?) } /// Find a message by sequence number. @@ -388,6 +399,7 @@ impl<'a> From<(&'a Account, &'a Mbox<'a>)> for ImapService<'a> { mbox, sess: None, _raw_mboxes_cache: None, + _raw_msgs_cache: None, } } } diff --git a/src/domain/mbox/attr_entity.rs b/src/domain/mbox/attr_entity.rs index fa1fb43..eae8d1b 100644 --- a/src/domain/mbox/attr_entity.rs +++ b/src/domain/mbox/attr_entity.rs @@ -2,7 +2,7 @@ //! //! 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 std::{ borrow::Cow, diff --git a/src/domain/mbox/mbox_arg.rs b/src/domain/mbox/mbox_arg.rs index 69aa7ee..23ace1e 100644 --- a/src/domain/mbox/mbox_arg.rs +++ b/src/domain/mbox/mbox_arg.rs @@ -9,13 +9,13 @@ use log::trace; /// Represents the mailbox commands. #[derive(Debug, PartialEq, Eq)] -pub(crate) enum Cmd { +pub enum Cmd { /// Represents the list mailboxes command. List, } /// Defines the mailbox command matcher. -pub(crate) fn matches(m: &clap::ArgMatches) -> Result> { +pub fn matches(m: &clap::ArgMatches) -> Result> { if let Some(_) = m.subcommand_matches("mailboxes") { trace!("mailboxes subcommand matched"); return Ok(Some(Cmd::List)); @@ -25,14 +25,14 @@ pub(crate) fn matches(m: &clap::ArgMatches) -> Result> { } /// Contains mailbox subcommands. -pub(crate) fn subcmds<'a>() -> Vec> { +pub fn subcmds<'a>() -> Vec> { vec![clap::SubCommand::with_name("mailboxes") .aliases(&["mailbox", "mboxes", "mbox", "mb", "m"]) .about("Lists mailboxes")] } /// 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") .short("m") .long("mailbox") @@ -42,7 +42,7 @@ pub(crate) fn source_arg<'a>() -> clap::Arg<'a, 'a> { } /// 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") .help("Specifies the targetted mailbox") .value_name("TARGET") diff --git a/src/domain/mbox/mbox_entity.rs b/src/domain/mbox/mbox_entity.rs index d951fe3..f809807 100644 --- a/src/domain/mbox/mbox_entity.rs +++ b/src/domain/mbox/mbox_entity.rs @@ -14,7 +14,7 @@ use 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. #[derive(Debug, Default, PartialEq, Eq, Serialize)] diff --git a/src/domain/mbox/mbox_handler.rs b/src/domain/mbox/mbox_handler.rs index a299600..86209a7 100644 --- a/src/domain/mbox/mbox_handler.rs +++ b/src/domain/mbox/mbox_handler.rs @@ -77,11 +77,11 @@ mod tests { unimplemented!() } - fn get_msgs(&mut self, _: &usize, _: &usize) -> Result { + fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result { unimplemented!() } - fn find_msgs(&mut self, _: &str, _: &usize, _: &usize) -> Result { + fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result { unimplemented!() } diff --git a/src/domain/msg/envelope_entity.rs b/src/domain/msg/envelope_entity.rs index d1ffebf..2b49eea 100644 --- a/src/domain/msg/envelope_entity.rs +++ b/src/domain/msg/envelope_entity.rs @@ -1,16 +1,18 @@ use anyhow::{anyhow, Context, Error, Result}; use serde::Serialize; -use std::convert::TryFrom; +use std::{borrow::Cow, convert::TryFrom}; use crate::{ domain::msg::{Flag, Flags}, 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 /// is mostly used for listings. #[derive(Debug, Default, Serialize)] -pub struct Envelope { +pub struct Envelope<'a> { /// The sequence number of the message. /// /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2 @@ -20,7 +22,7 @@ pub struct Envelope { pub flags: Flags, /// The subject of the message. - pub subject: String, + pub subject: Cow<'a, str>, /// The sender of the message. pub sender: String, @@ -31,10 +33,10 @@ pub struct Envelope { pub date: Option, } -impl<'a> TryFrom<&'a imap::types::Fetch> for Envelope { +impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> { type Error = Error; - fn try_from(fetch: &'a imap::types::Fetch) -> Result { + fn try_from(fetch: &'a RawEnvelope) -> Result { let envelope = fetch .envelope() .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())?; // Get the subject - let subject = envelope + let subject: Cow = envelope .subject .as_ref() .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 {}", fetch.message )) - })?; + })? + .into(); // Get the sender 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 { Row::new() .cell(Cell::new("ID").bold().underline().white()) diff --git a/src/domain/msg/envelopes_entity.rs b/src/domain/msg/envelopes_entity.rs index 139505e..2976916 100644 --- a/src/domain/msg/envelopes_entity.rs +++ b/src/domain/msg/envelopes_entity.rs @@ -1,5 +1,4 @@ use anyhow::{Error, Result}; -use imap::types::{Fetch, ZeroCopy}; use serde::Serialize; use std::{ convert::TryFrom, @@ -7,24 +6,29 @@ use std::{ ops::Deref, }; -use crate::{domain::msg::Envelope, ui::Table}; +use crate::{ + domain::{msg::Envelope, RawEnvelope}, + ui::Table, +}; + +pub type RawEnvelopes = imap::types::ZeroCopy>; /// Representation of a list of envelopes. #[derive(Debug, Default, Serialize)] -pub struct Envelopes(pub Vec); +pub struct Envelopes<'a>(pub Vec>); -impl Deref for Envelopes { - type Target = Vec; +impl<'a> Deref for Envelopes<'a> { + type Target = Vec>; fn deref(&self) -> &Self::Target { &self.0 } } -impl TryFrom>> for Envelopes { +impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> { type Error = Error; - fn try_from(fetches: ZeroCopy>) -> Result { + fn try_from(fetches: &'a RawEnvelopes) -> Result { let mut envelopes = vec![]; for fetch in fetches.iter().rev() { @@ -35,7 +39,7 @@ impl TryFrom>> for Envelopes { } } -impl Display for Envelopes { +impl<'a> Display for Envelopes<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "\n{}", Table::render(&self)) } diff --git a/src/domain/msg/flag_arg.rs b/src/domain/msg/flag_arg.rs index e849a0a..c9ae723 100644 --- a/src/domain/msg/flag_arg.rs +++ b/src/domain/msg/flag_arg.rs @@ -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 clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; @@ -11,37 +12,40 @@ use crate::domain::msg::msg_arg; type SeqRange<'a> = &'a str; type Flags<'a> = Vec<&'a str>; -/// Message flag commands. +/// Represents the flag commands. pub enum Command<'a> { - Set(SeqRange<'a>, Flags<'a>), + /// Represents the add flags command. 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>), } -/// Message flag command matcher. +/// Defines the flag command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { 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(); - trace!(r#"seq range: "{:?}""#, seq_range); + trace!(r#"seq range: "{}""#, seq_range); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); trace!(r#"flags: "{:?}""#, flags); return Ok(Some(Command::Add(seq_range, flags))); } 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(); - trace!(r#"seq range: "{:?}""#, seq_range); + trace!(r#"seq range: "{}""#, seq_range); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); trace!(r#"flags: "{:?}""#, flags); return Ok(Some(Command::Set(seq_range, flags))); } 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(); - trace!(r#"seq range: "{:?}""#, seq_range); + trace!(r#"seq range: "{}""#, seq_range); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); trace!(r#"flags: "{:?}""#, flags); return Ok(Some(Command::Remove(seq_range, flags))); @@ -50,7 +54,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { Ok(None) } -/// Message flag flags argument. +/// Defines the flags argument. fn flags_arg<'a>() -> Arg<'a, 'a> { Arg::with_name("flags") .help("IMAP flags") @@ -60,7 +64,7 @@ fn flags_arg<'a>() -> Arg<'a, 'a> { .required(true) } -/// Message flag subcommands. +/// Contains flag subcommands. pub fn subcmds<'a>() -> Vec> { vec![SubCommand::with_name("flag") .aliases(&["flags", "flg"]) @@ -68,19 +72,21 @@ pub fn subcmds<'a>() -> Vec> { .setting(AppSettings::SubcommandRequiredElseHelp) .subcommand( SubCommand::with_name("add") + .aliases(&["a"]) .about("Adds flags to a message") .arg(msg_arg::seq_range_arg()) .arg(flags_arg()), ) .subcommand( SubCommand::with_name("set") + .aliases(&["s", "change", "c"]) .about("Replaces all message flags") .arg(msg_arg::seq_range_arg()) .arg(flags_arg()), ) .subcommand( SubCommand::with_name("remove") - .aliases(&["rm"]) + .aliases(&["rem", "rm", "r", "delete", "del", "d"]) .about("Removes flags from a message") .arg(msg_arg::seq_range_arg()) .arg(flags_arg()), diff --git a/src/domain/msg/flag_entity.rs b/src/domain/msg/flag_entity.rs index b8474c8..127fdb4 100644 --- a/src/domain/msg/flag_entity.rs +++ b/src/domain/msg/flag_entity.rs @@ -1,10 +1,15 @@ pub use imap::types::Flag; use serde::ser::{Serialize, Serializer}; -/// Serializable wrapper arround [`imap::types::Flag`]. +/// Represents a serializable `imap::types::Flag`. #[derive(Debug, PartialEq, Eq, Clone)] 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> { fn serialize(&self, serializer: S) -> Result where diff --git a/src/domain/msg/flag_handler.rs b/src/domain/msg/flag_handler.rs index 8591fa8..c14d114 100644 --- a/src/domain/msg/flag_handler.rs +++ b/src/domain/msg/flag_handler.rs @@ -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 crate::{ - domain::{imap::ImapServiceInterface, msg::Flags}, + domain::{Flags, ImapServiceInterface}, 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 `\`. pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( 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 `\`. pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( 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 `\`. pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( seq_range: &'a str, diff --git a/src/domain/msg/flags_entity.rs b/src/domain/msg/flags_entity.rs index 2c746e9..2ed3872 100644 --- a/src/domain/msg/flags_entity.rs +++ b/src/domain/msg/flags_entity.rs @@ -10,12 +10,13 @@ use std::{ 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)] pub struct Flags(pub HashSet>); 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 { let mut flags = String::new(); 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> = 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> for Flags { fn from(flags: Vec<&'a str>) -> Self { let mut map: HashSet> = HashSet::new(); @@ -185,6 +144,7 @@ impl<'a> From> for Flags { } } +// FIXME //#[cfg(test)] //mod tests { // use crate::domain::msg::flag::entity::Flags; diff --git a/src/domain/msg/mod.rs b/src/domain/msg/mod.rs index 2a526d6..ad12d4b 100644 --- a/src/domain/msg/mod.rs +++ b/src/domain/msg/mod.rs @@ -43,9 +43,6 @@ pub use tpl_arg::TplOverride; pub mod tpl_handler; -pub mod tpl_entity; -pub use tpl_entity::*; - pub mod msg_entity; pub use msg_entity::*; diff --git a/src/domain/msg/msg_arg.rs b/src/domain/msg/msg_arg.rs index a118439..6d9b003 100644 --- a/src/domain/msg/msg_arg.rs +++ b/src/domain/msg/msg_arg.rs @@ -32,7 +32,7 @@ pub enum Command<'a> { Move(Seq<'a>, Mbox<'a>), Read(Seq<'a>, TextMime<'a>, Raw), Reply(Seq<'a>, All, AttachmentsPaths<'a>), - Save(Mbox<'a>, RawMsg<'a>), + Save(RawMsg<'a>), Search(Query, Option, Page), Send(RawMsg<'a>), Write(AttachmentsPaths<'a>), @@ -123,11 +123,9 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { if let Some(m) = m.subcommand_matches("save") { debug!("save command matched"); - let msg = m.value_of("message").unwrap(); - debug!("message: {}", &msg); - let mbox = m.value_of("mbox-target").unwrap(); - debug!("target mailbox: `{:?}`", mbox); - return Ok(Some(Command::Save(mbox, msg))); + let msg = m.value_of("message").unwrap_or_default(); + trace!("message: {}", msg); + return Ok(Some(Command::Save(msg))); } if let Some(m) = m.subcommand_matches("search") { @@ -197,7 +195,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } /// 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") .help("Specifies the targetted message") .value_name("SEQ") @@ -205,7 +203,7 @@ pub(crate) fn seq_arg<'a>() -> Arg<'a, 'a> { } /// 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") .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.") @@ -214,7 +212,7 @@ pub(crate) fn seq_range_arg<'a>() -> Arg<'a, 'a> { } /// 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") .help("Includes all recipients") .short("A") diff --git a/src/domain/msg/msg_entity.rs b/src/domain/msg/msg_entity.rs index edab4d6..0afc54e 100644 --- a/src/domain/msg/msg_entity.rs +++ b/src/domain/msg/msg_entity.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, FixedOffset}; use html_escape; use imap::types::Flag; use lettre::message::{Attachment, MultiPart, SinglePart}; +use log::trace; use regex::Regex; use rfc2047_decoder; use std::{ @@ -18,7 +19,7 @@ use crate::{ domain::{ imap::ImapServiceInterface, mbox::Mbox, - msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, Tpl, TplOverride}, + msg::{msg_utils, BinaryPart, Flags, Part, Parts, TextPlainPart, TplOverride}, smtp::SmtpServiceInterface, }, output::OutputServiceInterface, @@ -290,9 +291,9 @@ impl Msg { } fn _edit_with_editor(&self, account: &Account) -> Result { - let tpl = Tpl::from_msg(TplOverride::default(), self, account); + let tpl = self.to_tpl(TplOverride::default(), account); let tpl = editor::open_with_tpl(tpl)?; - Self::try_from(&tpl) + Self::from_tpl(&tpl) } pub fn edit_with_editor< @@ -314,7 +315,7 @@ impl Msg { Ok(choice) => match choice { PreEditChoice::Edit => { let tpl = editor::open_with_draft()?; - self.merge_with(Msg::try_from(&tpl)?); + self.merge_with(Msg::from_tpl(&tpl)?); break; } PreEditChoice::Discard => { @@ -355,7 +356,7 @@ impl Msg { Ok(PostEditChoice::RemoteDraft) => { let mbox = Mbox::new("Drafts"); 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)?; msg_utils::remove_local_draft()?; output.print("Message successfully saved to Drafts")?; @@ -439,12 +440,95 @@ impl Msg { } } } -} -impl TryFrom<&Tpl> for Msg { - type Error = Error; + pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> String { + let mut tpl = String::default(); - fn try_from(tpl: &Tpl) -> Result { + 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::>() + .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::>() + .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::>() + .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 { let mut msg = Msg::default(); let parsed_msg = diff --git a/src/domain/msg/msg_handler.rs b/src/domain/msg/msg_handler.rs index a09ed46..b444151 100644 --- a/src/domain/msg/msg_handler.rs +++ b/src/domain/msg/msg_handler.rs @@ -19,7 +19,7 @@ use crate::{ domain::{ imap::ImapServiceInterface, mbox::Mbox, - msg::{Flags, Msg, Part, TextPlainPart, Tpl}, + msg::{Flags, Msg, Part, TextPlainPart}, smtp::SmtpServiceInterface, }, 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); trace!("page size: {}", page_size); - let msgs = imap.get_msgs(&page_size, &page)?; + let msgs = imap.fetch_envelopes(&page_size, &page)?; trace!("messages: {:#?}", msgs); output.print(msgs) } @@ -244,14 +244,26 @@ pub fn reply< } /// Save a raw message to the targetted mailbox. -pub fn save<'a, ImapService: ImapServiceInterface<'a>>( - mbox: &str, - msg: &str, +pub fn save<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface<'a>>( + mbox: &Mbox, + raw_msg: &str, + output: &OutputService, imap: &mut ImapService, ) -> 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::>() + .join("\r\n") + }; + 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. @@ -261,12 +273,12 @@ pub fn search<'a, OutputService: OutputServiceInterface, ImapService: ImapServic page: usize, account: &Account, output: &OutputService, - imap: &mut ImapService, + imap: &'a mut ImapService, ) -> Result<()> { let page_size = page_size.unwrap_or(account.default_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); output.print(msgs) } @@ -295,8 +307,7 @@ pub fn send< .join("\r\n") }; - let tpl = Tpl(raw_msg.to_string()); - let msg = Msg::try_from(&tpl)?; + let msg = Msg::from_tpl(&raw_msg.to_string())?; let envelope: lettre::address::Envelope = msg.try_into()?; smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?; debug!("message sent!"); diff --git a/src/domain/msg/tpl_entity.rs b/src/domain/msg/tpl_entity.rs deleted file mode 100644 index 80e2cec..0000000 --- a/src/domain/msg/tpl_entity.rs +++ /dev/null @@ -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::>() - .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::>() - .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::>() - .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()) - } -} diff --git a/src/domain/msg/tpl_handler.rs b/src/domain/msg/tpl_handler.rs index 614ff5d..26105cf 100644 --- a/src/domain/msg/tpl_handler.rs +++ b/src/domain/msg/tpl_handler.rs @@ -8,7 +8,7 @@ use crate::{ config::Account, domain::{ imap::ImapServiceInterface, - msg::{Msg, Tpl, TplOverride}, + msg::{Msg, TplOverride}, }, output::OutputServiceInterface, }; @@ -19,8 +19,7 @@ pub fn new<'a, OutputService: OutputServiceInterface>( account: &'a Account, output: &'a OutputService, ) -> Result<()> { - let msg = Msg::default(); - let tpl = Tpl::from_msg(opts, &msg, account); + let tpl = Msg::default().to_tpl(opts, account); output.print(tpl) } @@ -33,8 +32,10 @@ pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapService output: &'a OutputService, imap: &'a mut ImapService, ) -> Result<()> { - let msg = imap.find_msg(seq)?.into_reply(all, account)?; - let tpl = Tpl::from_msg(opts, &msg, account); + let tpl = imap + .find_msg(seq)? + .into_reply(all, account)? + .to_tpl(opts, account); output.print(tpl) } @@ -46,7 +47,9 @@ pub fn forward<'a, OutputService: OutputServiceInterface, ImapService: ImapServi output: &'a OutputService, imap: &'a mut ImapService, ) -> Result<()> { - let msg = imap.find_msg(seq)?.into_forward(account)?; - let tpl = Tpl::from_msg(opts, &msg, account); + let tpl = imap + .find_msg(seq)? + .into_forward(account)? + .to_tpl(opts, account); output.print(tpl) } diff --git a/src/main.rs b/src/main.rs index 59fc1a6..4d8e564 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,8 +119,8 @@ fn main() -> Result<()> { Some(msg_arg::Command::Reply(seq, all, atts)) => { return msg_handler::reply(seq, all, atts, &account, &output, &mut imap, &mut smtp); } - Some(msg_arg::Command::Save(mbox, msg)) => { - return msg_handler::save(mbox, msg, &mut imap); + Some(msg_arg::Command::Save(raw_msg)) => { + return msg_handler::save(&mbox, raw_msg, &output, &mut imap); } Some(msg_arg::Command::Search(query, page_size, page)) => { return msg_handler::search(query, page_size, page, &account, &output, &mut imap); diff --git a/src/ui/editor.rs b/src/ui/editor.rs index ac37783..703d221 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -2,9 +2,9 @@ use anyhow::{Context, Result}; use log::debug; 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 { +pub fn open_with_tpl(tpl: String) -> Result { let path = msg_utils::local_draft_path(); debug!("create draft"); @@ -20,13 +20,12 @@ pub fn open_with_tpl(tpl: Tpl) -> Result { let content = fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; - Ok(Tpl(content)) + Ok(content) } -pub fn open_with_draft() -> Result { +pub fn open_with_draft() -> Result { let path = msg_utils::local_draft_path(); - let content = + let tpl = fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; - let tpl = Tpl(content); open_with_tpl(tpl) }