mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-17 23:03:37 +00:00
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:
parent
45aac9fbec
commit
d9272917f5
19 changed files with 228 additions and 264 deletions
|
@ -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<TlsStream<TcpStream>>;
|
||||
|
@ -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<Mboxes>;
|
||||
fn get_msgs(&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(&mut self, 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_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>>;
|
||||
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<RawMboxes>,
|
||||
_raw_msgs_cache: Option<RawEnvelopes>,
|
||||
}
|
||||
|
||||
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<Envelopes> {
|
||||
fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes> {
|
||||
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<Envelopes> {
|
||||
fn fetch_envelopes_with(
|
||||
&'a mut self,
|
||||
query: &str,
|
||||
page_size: &usize,
|
||||
page: &usize,
|
||||
) -> Result<Envelopes> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Option<Cmd>> {
|
||||
pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
|
||||
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<Option<Cmd>> {
|
|||
}
|
||||
|
||||
/// 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")
|
||||
.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")
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -77,11 +77,11 @@ mod tests {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_msgs(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {
|
||||
fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn find_msgs(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
|
||||
fn fetch_envelopes_with(&mut self, _: &str, _: &usize, _: &usize) -> Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
}
|
||||
|
||||
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<Envelope> {
|
||||
fn try_from(fetch: &'a RawEnvelope) -> Result<Envelope> {
|
||||
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<str> = 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())
|
||||
|
|
|
@ -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<Vec<RawEnvelope>>;
|
||||
|
||||
/// Representation of a list of envelopes.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Envelopes(pub Vec<Envelope>);
|
||||
pub struct Envelopes<'a>(pub Vec<Envelope<'a>>);
|
||||
|
||||
impl Deref for Envelopes {
|
||||
type Target = Vec<Envelope>;
|
||||
impl<'a> Deref for Envelopes<'a> {
|
||||
type Target = Vec<Envelope<'a>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ZeroCopy<Vec<Fetch>>> for Envelopes {
|
||||
impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(fetches: ZeroCopy<Vec<Fetch>>) -> Result<Self> {
|
||||
fn try_from(fetches: &'a RawEnvelopes) -> Result<Self> {
|
||||
let mut envelopes = vec![];
|
||||
|
||||
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 {
|
||||
writeln!(f, "\n{}", Table::render(&self))
|
||||
}
|
||||
|
|
|
@ -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<Option<Command<'a>>> {
|
||||
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<Option<Command<'a>>> {
|
|||
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<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("flag")
|
||||
.aliases(&["flags", "flg"])
|
||||
|
@ -68,19 +72,21 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
.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()),
|
||||
|
|
|
@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Flag<'static>>);
|
||||
|
||||
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<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 {
|
||||
fn from(flags: Vec<&'a str>) -> Self {
|
||||
let mut map: HashSet<Flag<'static>> = HashSet::new();
|
||||
|
@ -185,6 +144,7 @@ impl<'a> From<Vec<&'a str>> for Flags {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME
|
||||
//#[cfg(test)]
|
||||
//mod tests {
|
||||
// use crate::domain::msg::flag::entity::Flags;
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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<PageSize>, Page),
|
||||
Send(RawMsg<'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") {
|
||||
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<Option<Command<'a>>> {
|
|||
}
|
||||
|
||||
/// 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")
|
||||
|
|
|
@ -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<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)?;
|
||||
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<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 parsed_msg =
|
||||
|
|
|
@ -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::<Vec<String>>()
|
||||
.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!");
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Tpl> {
|
||||
pub fn open_with_tpl(tpl: String) -> Result<String> {
|
||||
let path = msg_utils::local_draft_path();
|
||||
|
||||
debug!("create draft");
|
||||
|
@ -20,13 +20,12 @@ pub fn open_with_tpl(tpl: Tpl) -> Result<Tpl> {
|
|||
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<Tpl> {
|
||||
pub fn open_with_draft() -> Result<String> {
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue