fix new tpl sig

This commit is contained in:
Clément DOUIN 2021-09-18 21:45:26 +02:00
parent 8e2a703e2b
commit e065d8d905
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
9 changed files with 199 additions and 221 deletions

View file

@ -1,13 +1,13 @@
//! Module related to completion handling.
//!
//! This module gathers all completion actions triggered by the CLI.
//! This module gathers all completion commands.
use anyhow::{anyhow, Context, Result};
use clap::{App, Shell};
use std::{io, str::FromStr};
/// Generate completion script from the given [`clap::App`] for the given shell slice.
pub fn generate<'a>(shell: Option<&'a str>, mut app: App<'a, 'a>) -> Result<()> {
pub fn generate<'a>(mut app: App<'a, 'a>, shell: Option<&'a str>) -> Result<()> {
let shell = Shell::from_str(shell.unwrap_or_default())
.map_err(|err| anyhow!(err))
.context("cannot parse shell")?;

View file

@ -183,60 +183,6 @@ impl Config {
Ok(())
}
/// Returns the signature of the given acccount in combination witht the sigantion delimiter.
/// If the account doesn't have a signature, then the global signature is used.
///
/// # Example
/// ```
/// use himalaya::config::model::{Config, Account};
///
/// fn main() {
/// let config = Config {
/// signature: Some("Global signature".to_string()),
/// .. Config::default()
/// };
///
/// // a config without a global signature
/// let config_no_global = Config::default();
///
/// let account1 = Account::new_with_signature(Some("Account Name"), "mail@address.com", Some("Cya"));
/// let account2 = Account::new(Some("Bruh"), "mail@address.com");
///
/// // Hint: Don't forget the default signature delimiter: '\n-- \n'
/// assert_eq!(config.signature(&account1), Some("\n-- \nCya".to_string()));
/// assert_eq!(config.signature(&account2), Some("\n-- \nGlobal signature".to_string()));
///
/// assert_eq!(config_no_global.signature(&account2), None);
/// }
/// ```
pub fn signature(&self, account: &ConfigAccountEntry) -> Option<String> {
let default_sig_delim = String::from("-- \n");
let sig_delim = account
.signature_delimiter
.as_ref()
.or_else(|| self.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let sig = account
.signature
.as_ref()
.or_else(|| self.signature.as_ref());
sig.and_then(|sig| shellexpand::full(sig).ok())
.map(|sig| sig.to_string())
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| sig.map(|sig| sig.to_owned()))
.map(|sig| format!("\n{}{}", sig_delim, sig))
}
pub fn default_page_size(&self, account: &ConfigAccountEntry) -> usize {
account
.default_page_size
.as_ref()
.or_else(|| self.default_page_size.as_ref())
.or(Some(&DEFAULT_PAGE_SIZE))
.unwrap()
.to_owned()
}
pub fn exec_watch_cmds(&self, account: &ConfigAccountEntry) -> Result<()> {
let cmds = account
.watch_cmds
@ -551,10 +497,10 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
.or_else(|| config.signature.as_ref());
let signature = signature
.and_then(|sig| shellexpand::full(sig).ok())
.map(|sig| sig.to_string())
.map(String::from)
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| signature.map(|sig| sig.to_owned()))
.map(|sig| format!("\n{}{}", signature_delim, sig))
.map(|sig| format!("\n{}{}", signature_delim, sig.trim_end()))
.unwrap_or_default();
let account = Account {

View file

@ -7,16 +7,16 @@ use clap::{App, Arg, ArgMatches, SubCommand};
use log::debug;
/// Mailbox commands.
pub enum Commands {
pub enum Command {
/// List all available mailboxes.
List,
}
/// Mailbox command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Commands>> {
pub fn matches(m: &ArgMatches) -> Result<Option<Command>> {
if let Some(_) = m.subcommand_matches("mailboxes") {
debug!("mailboxes command matched");
return Ok(Some(Commands::List));
return Ok(Some(Command::List));
}
Ok(None)

View file

@ -34,8 +34,8 @@ pub enum Command<'a> {
Send(RawMsg<'a>),
Write(AttachmentsPaths<'a>),
Flag(msg::flag::arg::Command<'a>),
Tpl(msg::tpl::arg::Command<'a>),
Flag(Option<msg::flag::arg::Command<'a>>),
Tpl(Option<msg::tpl::arg::Command<'a>>),
}
/// Message command matcher.
@ -63,10 +63,6 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
return Ok(Some(Command::Delete(uid)));
}
if let Some(m) = msg::flag::arg::matches(&m)? {
return Ok(Some(Command::Flag(m)));
}
if let Some(m) = m.subcommand_matches("forward") {
debug!("forward command matched");
let uid = m.value_of("uid").unwrap();
@ -176,10 +172,6 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
return Ok(Some(Command::Send(msg)));
}
if let Some(m) = msg::tpl::arg::matches(&m)? {
return Ok(Some(Command::Tpl(m)));
}
if let Some(m) = m.subcommand_matches("write") {
debug!("write command matched");
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
@ -187,6 +179,14 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
return Ok(Some(Command::Write(attachment_paths)));
}
if let Some(m) = m.subcommand_matches("template") {
return Ok(Some(Command::Tpl(msg::tpl::arg::matches(&m)?)));
}
if let Some(m) = m.subcommand_matches("flag") {
return Ok(Some(Command::Flag(msg::flag::arg::matches(&m)?)));
}
debug!("default list command matched");
Ok(Some(Command::List(None, 0)))
}

View file

@ -13,8 +13,8 @@ use std::fmt;
/// This part of the msg/msg would be stored in this struct.
#[derive(Clone, Serialize, Debug, PartialEq, Eq)]
pub struct Body {
/// The text version of a body (if available)
pub text: Option<String>,
/// The plain version of a body (if available)
pub plain: Option<String>,
/// The html version of a body (if available)
pub html: Option<String>,
@ -61,7 +61,7 @@ impl Body {
/// ```
pub fn new_with_text<S: ToString>(text: S) -> Self {
Self {
text: Some(text.to_string()),
plain: Some(text.to_string()),
html: None,
}
}
@ -85,7 +85,7 @@ impl Body {
/// ```
pub fn new_with_html<S: ToString>(html: S) -> Self {
Self {
text: None,
plain: None,
html: Some(html.to_string()),
}
}
@ -109,7 +109,7 @@ impl Body {
/// ```
pub fn new_with_both<S: ToString>(text: S, html: S) -> Self {
Self {
text: Some(text.to_string()),
plain: Some(text.to_string()),
html: Some(html.to_string()),
}
}
@ -119,7 +119,7 @@ impl Body {
impl Default for Body {
fn default() -> Self {
Self {
text: None,
plain: None,
html: None,
}
}
@ -127,7 +127,7 @@ impl Default for Body {
impl fmt::Display for Body {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let content = if let Some(text) = self.text.clone() {
let content = if let Some(text) = self.plain.clone() {
text
} else if let Some(html) = self.html.clone() {
html

View file

@ -16,8 +16,7 @@ use crate::ui::editor;
use serde::Serialize;
use lettre::message::{
header::ContentTransferEncoding, header::ContentType, Attachment as lettre_Attachment, Mailbox,
Message, MultiPart, SinglePart,
header::ContentType, Attachment as lettre_Attachment, Mailbox, Message, MultiPart, SinglePart,
};
use std::{
@ -64,7 +63,7 @@ impl fmt::Display for MsgSerialized {
/// This struct represents a whole msg with its attachments, body-content
/// and its headers.
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
pub struct Msg {
/// All added attachments are listed in this vector.
pub attachments: Vec<Attachment>,
@ -80,6 +79,9 @@ pub struct Msg {
/// This includes the general content text and the signature.
pub body: Body,
/// The signature of the message.
pub sig: String,
/// The UID of the msg. In general, a message should already have one, unless you're writing a
/// new message, then we're generating it.
uid: Option<u32>,
@ -167,11 +169,6 @@ impl Msg {
if headers.from.is_empty() {
headers.from = vec![account.address()];
}
if let None = headers.signature {
headers.signature = Some(account.signature.to_owned());
}
let body = Body::new_with_text(if let Some(sig) = headers.signature.as_ref() {
format!("\n{}", sig)
} else {
@ -181,6 +178,7 @@ impl Msg {
Self {
headers,
body,
sig: account.signature.to_owned(),
..Self::default()
}
}
@ -280,7 +278,7 @@ impl Msg {
// each line which includes a string.
let mut new_body = self
.body
.text
.plain
.clone()
.unwrap_or_default()
.lines()
@ -354,7 +352,11 @@ impl Msg {
// apply a line which should indicate where the forwarded message begins
body.push_str(&format!(
"\n---------- Forwarded Message ----------\n{}",
self.body.text.clone().unwrap_or_default().replace("\r", ""),
self.body
.plain
.clone()
.unwrap_or_default()
.replace("\r", ""),
));
body.push_str(&account.signature);
@ -685,16 +687,16 @@ impl Msg {
let mut msg_parts = MultiPart::mixed().build();
// -- Body --
if self.body.text.is_some() && self.body.html.is_some() {
if self.body.plain.is_some() && self.body.html.is_some() {
msg_parts = msg_parts.multipart(MultiPart::alternative_plain_html(
self.body.text.clone().unwrap(),
self.body.plain.clone().unwrap(),
self.body.html.clone().unwrap(),
));
} else {
let msg_body = SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.header(self.headers.encoding)
.body(self.body.text.clone().unwrap_or_default());
.body(self.body.plain.clone().unwrap_or_default());
msg_parts = msg_parts.singlepart(msg_body);
}
@ -740,42 +742,16 @@ impl Msg {
Ok(raw_message)
}
/// Returns the [`ContentTransferEncoding`] of the body.
pub fn get_encoding(&self) -> ContentTransferEncoding {
self.headers.encoding
}
/// Returns the whole message: Header + Body as a String
pub fn get_full_message(&self) -> String {
format!("{}\n{}", self.headers.get_header_as_string(), self.body)
}
}
// -- Traits --
impl Default for Msg {
fn default() -> Self {
Self {
attachments: Vec::new(),
flags: Flags::default(),
headers: Headers::default(),
body: Body::default(),
// the uid is generated in the "to_sendable_msg" function if the server didn't apply a
// message id to it.
uid: None,
date: None,
raw: Vec::new(),
}
}
}
impl fmt::Display for Msg {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"{}\n{}",
"{}\n{}{}",
self.headers.get_header_as_string(),
self.body
self.body,
self.sig
)
}
}
@ -880,7 +856,7 @@ impl TryFrom<&Fetch> for Msg {
// don't. This condition hits, if the body isn't in a multipart, so we can
// immediately fetch the body from the first part of the mail.
match parsed.ctype.mimetype.as_ref() {
"text/plain" => body.text = parsed.get_body().ok(),
"text/plain" => body.plain = parsed.get_body().ok(),
"text/html" => body.html = parsed.get_body().ok(),
_ => (),
};
@ -889,8 +865,8 @@ impl TryFrom<&Fetch> for Msg {
// now it might happen, that the body is *in* a multipart, if
// that's the case, look, if we've already applied a body
// (body.is_empty()) and set it, if needed
if body.text.is_none() && subpart.ctype.mimetype == "text/plain" {
body.text = subpart.get_body().ok();
if body.plain.is_none() && subpart.ctype.mimetype == "text/plain" {
body.plain = subpart.get_body().ok();
} else if body.html.is_none() && subpart.ctype.mimetype == "text/html" {
body.html = subpart.get_body().ok();
}
@ -918,6 +894,7 @@ impl TryFrom<&Fetch> for Msg {
uid,
date,
raw,
..Self::default()
})
}
}

View file

@ -3,26 +3,61 @@
//! This module provides subcommands, arguments and a command matcher related to message template.
use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand};
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand, Values};
use log::debug;
use crate::domain::msg::{self, arg::uid_arg};
type Subject<'a> = Option<&'a str>;
type From<'a> = Option<Values<'a>>;
type To<'a> = Option<Values<'a>>;
type Cc<'a> = Option<Values<'a>>;
type Bcc<'a> = Option<Values<'a>>;
type Headers<'a> = Option<Values<'a>>;
type Body<'a> = Option<&'a str>;
type Signature<'a> = Option<&'a str>;
type Uid<'a> = &'a str;
type All = bool;
/// Message template commands.
pub enum Command<'a> {
New,
New(
Subject<'a>,
From<'a>,
To<'a>,
Cc<'a>,
Bcc<'a>,
Headers<'a>,
Body<'a>,
Signature<'a>,
),
Reply(Uid<'a>, All),
Forward(Uid<'a>),
}
/// Message template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(_) = m.subcommand_matches("new") {
if let Some(m) = m.subcommand_matches("new") {
debug!("new command matched");
return Ok(Some(Command::New));
let subject = m.value_of("subject");
debug!("subject: `{:?}`", subject);
let from = m.values_of("from");
debug!("from: `{:?}`", from);
let to = m.values_of("to");
debug!("to: `{:?}`", to);
let cc = m.values_of("cc");
debug!("cc: `{:?}`", cc);
let bcc = m.values_of("bcc");
debug!("bcc: `{:?}`", bcc);
let headers = m.values_of("header");
debug!("headers: `{:?}`", headers);
let body = m.value_of("body");
debug!("body: `{:?}`", body);
let sig = m.value_of("signature");
debug!("signature: `{:?}`", sig);
return Ok(Some(Command::New(
subject, from, to, cc, bcc, headers, body, sig,
)));
}
if let Some(m) = m.subcommand_matches("reply") {
@ -100,6 +135,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("template")
.aliases(&["tpl"])
.about("Generates a message template")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("new")
.aliases(&["n"])

View file

@ -1,25 +1,42 @@
use std::convert::TryFrom;
use std::{
collections::HashMap,
convert::TryFrom,
io::{self, BufRead},
};
use anyhow::Result;
use log::trace;
use atty::Stream;
use clap::Values;
use log::{debug, trace};
use crate::{
config::entity::Account,
domain::{
imap::service::ImapServiceInterface,
msg::entity::{Msg, MsgSerialized},
msg::{
body::Body,
entity::{Msg, MsgSerialized},
headers::Headers,
},
},
output::service::OutputServiceInterface,
};
pub fn new<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
pub fn new<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
subject: Option<&'a str>,
from: Option<Values<'a>>,
to: Option<Values<'a>>,
cc: Option<Values<'a>>,
bcc: Option<Values<'a>>,
headers: Option<Values<'a>>,
body: Option<&'a str>,
sig: Option<&'a str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let msg = Msg::new(&account);
// FIXME
// override_msg_with_args(&mut msg, &matches);
let mut msg = Msg::new(&account);
override_msg_with_args(&mut msg, subject, from, to, cc, bcc, headers, body, sig);
trace!("message: {:#?}", msg);
output.print(MsgSerialized::try_from(&msg)?)?;
imap.logout()?;
@ -62,89 +79,85 @@ pub fn forward<OutputService: OutputServiceInterface, ImapService: ImapServiceIn
// == Helper functions ==
// -- Template Subcommands --
// These functions are more used for the "template" subcommand
// fn override_msg_with_args(msg: &mut Msg) {
// // -- Collecting credentials --
// let from: Vec<String> = match matches.values_of("from") {
// Some(from) => from.map(|arg| arg.to_string()).collect(),
// None => msg.headers.from.clone(),
// };
fn override_msg_with_args<'a>(
msg: &mut Msg,
subject: Option<&'a str>,
from: Option<Values<'a>>,
to: Option<Values<'a>>,
cc: Option<Values<'a>>,
bcc: Option<Values<'a>>,
headers: Option<Values<'a>>,
body: Option<&'a str>,
sig: Option<&'a str>,
) {
// -- Collecting credentials --
let from: Vec<String> = match from {
Some(from) => from.map(|arg| arg.to_string()).collect(),
None => msg.headers.from.clone(),
};
// let to: Vec<String> = match matches.values_of("to") {
// Some(to) => to.map(|arg| arg.to_string()).collect(),
// None => Vec::new(),
// };
let to: Vec<String> = match to {
Some(to) => to.map(|arg| arg.to_string()).collect(),
None => Vec::new(),
};
// let subject = matches
// .value_of("subject")
// .and_then(|subject| Some(subject.to_string()));
let subject = subject.map(String::from);
let cc: Option<Vec<String>> = cc.map(|cc| cc.map(|arg| arg.to_string()).collect());
let bcc: Option<Vec<String>> = bcc.map(|bcc| bcc.map(|arg| arg.to_string()).collect());
let signature = sig.map(String::from).or(msg.headers.signature.to_owned());
// let cc: Option<Vec<String>> = matches
// .values_of("cc")
// .and_then(|cc| Some(cc.map(|arg| arg.to_string()).collect()));
let custom_headers: Option<HashMap<String, Vec<String>>> = {
if let Some(matched_headers) = headers {
let mut custom_headers: HashMap<String, Vec<String>> = HashMap::new();
// let bcc: Option<Vec<String>> = matches
// .values_of("bcc")
// .and_then(|bcc| Some(bcc.map(|arg| arg.to_string()).collect()));
// collect the custom headers
for header in matched_headers {
let mut header = header.split(":");
let key = header.next().unwrap_or_default();
let val = header.next().unwrap_or_default().trim_start();
// let signature = matches
// .value_of("signature")
// .and_then(|signature| Some(signature.to_string()))
// .or(msg.headers.signature.clone());
custom_headers.insert(key.to_string(), vec![val.to_string()]);
}
// let custom_headers: Option<HashMap<String, Vec<String>>> = {
// if let Some(matched_headers) = matches.values_of("header") {
// let mut custom_headers: HashMap<String, Vec<String>> = HashMap::new();
Some(custom_headers)
} else {
None
}
};
// // collect the custom headers
// for header in matched_headers {
// let mut header = header.split(":");
// let key = header.next().unwrap_or_default();
// let val = header.next().unwrap_or_default().trim_start();
let body = {
if atty::isnt(Stream::Stdin) {
let body = io::stdin()
.lock()
.lines()
.filter_map(|line| line.ok())
.map(|line| line.to_string())
.collect::<Vec<String>>()
.join("\n");
debug!("overriden body from stdin: {:?}", body);
body
} else if let Some(body) = body {
debug!("overriden body: {:?}", body);
body.to_string()
} else {
String::new()
}
};
// debug!("overriden header: {}={}", key, val);
let body = Body::new_with_text(body);
// custom_headers.insert(key.to_string(), vec![val.to_string()]);
// }
// -- Creating and printing --
let headers = Headers {
from,
subject,
to,
cc,
bcc,
signature,
custom_headers,
..msg.headers.clone()
};
// Some(custom_headers)
// } else {
// None
// }
// };
// let body = {
// if atty::isnt(Stream::Stdin) {
// let body = io::stdin()
// .lock()
// .lines()
// .filter_map(|line| line.ok())
// .map(|line| line.to_string())
// .collect::<Vec<String>>()
// .join("\n");
// debug!("overriden body from stdin: {:?}", body);
// body
// } else if let Some(body) = matches.value_of("body") {
// debug!("overriden body: {:?}", body);
// body.to_string()
// } else {
// String::new()
// }
// };
// let body = Body::new_with_text(body);
// // -- Creating and printing --
// let headers = Headers {
// from,
// subject,
// to,
// cc,
// bcc,
// signature,
// custom_headers,
// ..msg.headers.clone()
// };
// msg.headers = headers;
// msg.body = body;
// }
msg.headers = headers;
msg.body = body;
}

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use clap;
use clap::{self, AppSettings};
use env_logger;
use std::{convert::TryFrom, env};
use url::Url;
@ -24,8 +24,10 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
.version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS"))
.args(&output::arg::args())
.setting(AppSettings::GlobalVersion)
.setting(AppSettings::SubcommandRequiredElseHelp)
.args(&config::arg::args())
.args(&output::arg::args())
.arg(mbox::arg::source_arg())
.subcommands(compl::arg::subcmds())
.subcommands(imap::arg::subcmds())
@ -59,7 +61,7 @@ fn main() -> Result<()> {
// See https://github.com/soywod/himalaya/issues/115.
match compl::arg::matches(&m)? {
Some(compl::arg::Command::Generate(shell)) => {
return compl::handler::generate(shell, create_app());
return compl::handler::generate(create_app(), shell);
}
_ => (),
}
@ -84,7 +86,7 @@ fn main() -> Result<()> {
// Check mailbox matches.
match mbox::arg::matches(&m)? {
Some(mbox::arg::Commands::List) => {
Some(mbox::arg::Command::List) => {
return mbox::handler::list(&output, &mut imap);
}
_ => (),
@ -130,26 +132,30 @@ fn main() -> Result<()> {
}
Some(msg::arg::Command::Flag(m)) => match m {
msg::flag::arg::Command::Set(uid, flags) => {
Some(msg::flag::arg::Command::Set(uid, flags)) => {
return msg::flag::handler::set(uid, flags, &mut imap);
}
msg::flag::arg::Command::Add(uid, flags) => {
Some(msg::flag::arg::Command::Add(uid, flags)) => {
return msg::flag::handler::add(uid, flags, &mut imap);
}
msg::flag::arg::Command::Remove(uid, flags) => {
Some(msg::flag::arg::Command::Remove(uid, flags)) => {
return msg::flag::handler::remove(uid, flags, &mut imap);
}
_ => (),
},
Some(msg::arg::Command::Tpl(m)) => match m {
msg::tpl::arg::Command::New => {
return msg::tpl::handler::new(&account, &output, &mut imap);
Some(msg::tpl::arg::Command::New(sub, from, to, cc, bcc, h, body, sig)) => {
return msg::tpl::handler::new(
sub, from, to, cc, bcc, h, body, sig, &account, &output, &mut imap,
);
}
msg::tpl::arg::Command::Reply(uid, all) => {
Some(msg::tpl::arg::Command::Reply(uid, all)) => {
return msg::tpl::handler::reply(uid, all, &account, &output, &mut imap);
}
msg::tpl::arg::Command::Forward(uid) => {
Some(msg::tpl::arg::Command::Forward(uid)) => {
return msg::tpl::handler::forward(uid, &account, &output, &mut imap);
}
_ => (),
},
_ => (),
}