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::{
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,
}
}
}

View file

@ -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,

View file

@ -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")

View file

@ -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)]

View file

@ -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!()
}

View file

@ -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())

View file

@ -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))
}

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 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()),

View file

@ -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

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 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,

View file

@ -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;

View file

@ -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::*;

View file

@ -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")

View file

@ -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 =

View file

@ -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!");

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,
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)
}

View file

@ -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);

View file

@ -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)
}