Add compose (pre-submission) hooks for validation/linting

compose-hooks run before submitting an e-mail.
They perform draft validation and/or transformations.
If a hook encounters an error or warning, it will show up as a notification.
The currently available hooks are:
- past-date-warn
  Warn if Date header value is far in the past or future.
- important-header-warn
  Warn if important headers (From, Date, To, Cc, Bcc) are missing or invalid.
- missing-attachment-warn
  Warn if Subject, draft body mention attachments but they are missing.
- empty-draft-warn
  Warn if draft has no subject and no body.

They can be disabled with [composing.disabled_compose_hooks] setting.
pull/211/head
Manos Pitsidianakis 2023-05-16 13:17:13 +03:00
parent 1f1ea30769
commit 8c671935f9
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
19 changed files with 596 additions and 582 deletions

1
Cargo.lock generated
View File

@ -1155,6 +1155,7 @@ dependencies = [
"structopt",
"svg",
"syn",
"tempfile",
"termion",
"toml",
"unicode-segmentation",

View File

@ -66,6 +66,7 @@ syn = { version = "1.0.92", features = [] }
[dev-dependencies]
regex = "1"
tempfile = "3.3"
[profile.release]
lto = "fat"

View File

@ -127,6 +127,7 @@ theme = "light"
Available options are listed below.
Default values are shown in parentheses.
.Sh ACCOUNTS
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic root_mailbox Ar String
The backend-specific path of the root_mailbox, usually INBOX.
@ -513,7 +514,8 @@ Useful only for mUTF-7 mailboxes.
.Pq Em "utf7", "utf-7", "utf8", "utf-8"
.El
.Sh COMPOSING
Composing specific options
Composing specific options.
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic send_mail Ar String|SmtpServerConf
Command to pipe new mail to (exit code must be 0 for success) or settings for an SMTP server connection.
@ -611,8 +613,30 @@ When used in a reply, the field body MAY start with the string "Re: " (from
the Latin "res", in the matter of) followed by the contents of the "Subject:"
field body of the original message.
.Ed
.It Ic disabled_compose_hooks Ar [String]
.Pq Em optional
Disabled compose-hooks.
compose-hooks run before submitting an e-mail.
They perform draft validation and/or transformations.
If a hook encounters an error or warning, it will show up as a notification.
The currently available hooks are:
.Bl -bullet -compact
.It
.Ic past-date-warn
— Warn if Date header value is far in the past or future.
.It
.Ic important-header-warn
— Warn if important headers (From, Date, To, Cc, Bcc) are missing or invalid.
.It
.Ic missing-attachment-warn
— Warn if Subject, draft body mention attachments but they are missing.
.It
.Ic empty-draft-warn
— Warn if draft has no subject and no body.
.El
.El
.Sh SHORTCUTS
Default values are shown in parentheses.
Shortcuts can take the following values:
.Bl -bullet -compact
.It
@ -1040,6 +1064,7 @@ toggle thread view visibility
.El
.sp
.Sh NOTIFICATIONS
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic enable Ar boolean
Enable notifications.
@ -1074,6 +1099,7 @@ Play sound file in notifications if possible.
.Pq Em none
.El
.Sh PAGER
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic headers_sticky Ar boolean
.Pq Em optional
@ -1128,6 +1154,7 @@ The URL will be given as the first argument of the command.
.Pq Em xdg-open
.El
.Sh LISTING
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic show_menu_scrollbar Ar boolean
.Pq Em optional
@ -1269,6 +1296,7 @@ no_sibling_leaf = " \\_"
.Ed
.sp
.Sh TAGS
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic colours Ar hash table String[Color]
.Pq Em optional
@ -1291,6 +1319,7 @@ colors = { signed="#Ff6600", replied="DeepSkyBlue4", draft="#f00", replied="8" }
"INBOX" = { tags.ignore_tags=["inbox", ] }
.Ed
.Sh PGP
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic auto_verify_signatures Ar boolean
Auto verify signed e-mail according to RFC3156
@ -1308,6 +1337,7 @@ Key to be used when signing/encrypting (not functional yet)
.Pq Em none
.El
.Sh TERMINAL
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic theme Ar String
.Pq Em optional
@ -1431,6 +1461,7 @@ progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] }
.Ed
.El
.Sh LOG
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic log_file Ar String
.Pq Em optional
@ -1467,6 +1498,7 @@ to
.Pq Em INFO
.El
.Sh SMTP Connections
Default values are shown in parentheses.
.Bl -tag -width 36n
.It Ic hostname Ar String
server hostname

View File

@ -67,14 +67,15 @@ pub struct MaildirPath {
impl Deref for MaildirPath {
type Target = PathBuf;
fn deref(&self) -> &PathBuf {
fn deref(&self) -> &Self::Target {
assert!(!(self.removed && self.modified.is_none()));
&self.buf
}
}
impl DerefMut for MaildirPath {
fn deref_mut(&mut self) -> &mut PathBuf {
fn deref_mut(&mut self) -> &mut Self::Target {
assert!(!(self.removed && self.modified.is_none()));
&mut self.buf
}
@ -98,13 +99,14 @@ pub struct HashIndex {
impl Deref for HashIndex {
type Target = HashMap<EnvelopeHash, MaildirPath>;
fn deref(&self) -> &HashMap<EnvelopeHash, MaildirPath> {
fn deref(&self) -> &Self::Target {
&self.index
}
}
impl DerefMut for HashIndex {
fn deref_mut(&mut self) -> &mut HashMap<EnvelopeHash, MaildirPath> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.index
}
}

View File

@ -444,7 +444,6 @@ impl Envelope {
if self.bcc.is_empty() {
self.other_headers
.get("Bcc")
.map(|s| s.as_str())
.unwrap_or_default()
.to_string()
} else {
@ -460,11 +459,7 @@ impl Envelope {
pub fn field_cc_to_string(&self) -> String {
if self.cc.is_empty() {
self.other_headers
.get("Cc")
.map(|s| s.as_str())
.unwrap_or_default()
.to_string()
self.other_headers.get("Cc").unwrap_or_default().to_string()
} else {
self.cc.iter().fold(String::new(), |mut acc, x| {
if !acc.is_empty() {
@ -480,7 +475,6 @@ impl Envelope {
if self.from.is_empty() {
self.other_headers
.get("From")
.map(|s| s.as_str())
.unwrap_or_default()
.to_string()
} else {
@ -508,11 +502,7 @@ impl Envelope {
pub fn field_to_to_string(&self) -> String {
if self.to.is_empty() {
self.other_headers
.get("To")
.map(|s| s.as_str())
.unwrap_or_default()
.to_string()
self.other_headers.get("To").unwrap_or_default().to_string()
} else {
self.to
.iter()
@ -532,7 +522,6 @@ impl Envelope {
if refs.is_empty() {
self.other_headers
.get("References")
.map(|s| s.as_str())
.unwrap_or_default()
.to_string()
} else {

View File

@ -491,6 +491,38 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
}
}
/// Reads file from given path, and returns an 'application/octet-stream'
/// AttachmentBuilder object
pub fn attachment_from_file<I>(path: &I) -> Result<AttachmentBuilder>
where
I: AsRef<OsStr>,
{
let path: PathBuf = Path::new(path).expand();
if !path.is_file() {
return Err(Error::new(format!("{} is not a file", path.display())));
}
let mut file = std::fs::File::open(&path)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
let mut attachment = AttachmentBuilder::default();
attachment
.set_raw(contents)
.set_body_to_raw()
.set_content_type(ContentType::Other {
name: path.file_name().map(|s| s.to_string_lossy().into()),
tag: if let Ok(mime_type) = query_mime_info(&path) {
mime_type
} else {
b"application/octet-stream".to_vec()
},
parameters: vec![],
});
Ok(attachment)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
@ -601,35 +633,3 @@ mod tests {
}
*/
}
/// Reads file from given path, and returns an 'application/octet-stream'
/// AttachmentBuilder object
pub fn attachment_from_file<I>(path: &I) -> Result<AttachmentBuilder>
where
I: AsRef<OsStr>,
{
let path: PathBuf = Path::new(path).expand();
if !path.is_file() {
return Err(Error::new(format!("{} is not a file", path.display())));
}
let mut file = std::fs::File::open(&path)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
let mut attachment = AttachmentBuilder::default();
attachment
.set_raw(contents)
.set_body_to_raw()
.set_content_type(ContentType::Other {
name: path.file_name().map(|s| s.to_string_lossy().into()),
tag: if let Ok(mime_type) = query_mime_info(&path) {
mime_type
} else {
b"application/octet-stream".to_vec()
},
parameters: vec![],
});
Ok(attachment)
}

View File

@ -252,8 +252,10 @@ impl HeaderMap {
(self.0).get_mut(HeaderNameType(key).borrow() as &dyn HeaderKey)
}
pub fn get(&self, key: &str) -> Option<&String> {
(self.0).get(HeaderNameType(key).borrow() as &dyn HeaderKey)
pub fn get(&self, key: &str) -> Option<&str> {
(self.0)
.get(HeaderNameType(key).borrow() as &dyn HeaderKey)
.map(|x| x.as_str())
}
pub fn contains_key(&self, key: &str) -> bool {
@ -274,7 +276,7 @@ impl Deref for HeaderMap {
}
impl DerefMut for HeaderMap {
fn deref_mut(&mut self) -> &mut IndexMap<HeaderName, String> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -87,7 +87,6 @@ pub fn list_id_header(envelope: &'_ Envelope) -> Option<&'_ str> {
.other_headers()
.get("List-ID")
.or_else(|| envelope.other_headers().get("List-Id"))
.map(String::as_str)
}
pub fn list_id(header: Option<&'_ str>) -> Option<&'_ str> {

View File

@ -477,9 +477,9 @@ impl Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}", self.summary)?;
write!(f, "{}", self.summary)?;
if let Some(details) = self.details.as_ref() {
write!(f, "{}", details)?;
write!(f, "\n{}", details)?;
}
if let Some(source) = self.source.as_ref() {
write!(f, "\nCaused by: {}", source)?;

View File

@ -19,10 +19,10 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
#[cfg(not(test))]
use std::{fs::OpenOptions, path::PathBuf};
use std::{
fs::OpenOptions,
io::{BufWriter, Write},
path::PathBuf,
sync::{
atomic::{AtomicU8, Ordering},
Arc, Mutex,
@ -31,8 +31,6 @@ use std::{
use log::{Level, LevelFilter, Log, Metadata, Record};
use crate::shellexpand::ShellExpandTrait;
#[derive(Copy, Clone, Default, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[repr(u8)]
pub enum LogLevel {
@ -137,6 +135,9 @@ pub enum Destination {
#[derive(Clone)]
pub struct StderrLogger {
#[cfg(test)]
dest: Arc<Mutex<BufWriter<std::io::Stderr>>>,
#[cfg(not(test))]
dest: Arc<Mutex<BufWriter<std::fs::File>>>,
level: Arc<AtomicU8>,
print_level: bool,
@ -163,6 +164,11 @@ impl Default for StderrLogger {
impl StderrLogger {
pub fn new(level: LogLevel) -> Self {
use std::sync::Once;
static INIT_STDERR_LOGGING: Once = Once::new();
#[cfg(not(test))]
let logger = {
let data_dir = xdg::BaseDirectories::with_prefix("meli").unwrap();
let log_file = OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */
@ -180,6 +186,16 @@ impl StderrLogger {
debug_dest: Destination::None,
}
};
#[cfg(test)]
let logger = {
StderrLogger {
dest: Arc::new(Mutex::new(BufWriter::new(std::io::stderr()).into())),
level: Arc::new(AtomicU8::new(level as u8)),
print_level: true,
print_module_names: true,
debug_dest: Destination::Stderr,
}
};
#[cfg(feature = "debug-tracing")]
log::set_max_level(
@ -191,7 +207,10 @@ impl StderrLogger {
);
#[cfg(not(feature = "debug-tracing"))]
log::set_max_level(LevelFilter::from(logger.log_level()));
INIT_STDERR_LOGGING.call_once(|| {
log::set_boxed_logger(Box::new(logger.clone())).unwrap();
});
logger
}
@ -199,7 +218,10 @@ impl StderrLogger {
self.level.load(Ordering::SeqCst).into()
}
#[cfg(not(test))]
pub fn change_log_dest(&mut self, path: PathBuf) {
use crate::shellexpand::ShellExpandTrait;
let path = path.expand(); // expand shell stuff
let mut dest = self.dest.lock().unwrap();
*dest = BufWriter::new(OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */

View File

@ -22,6 +22,7 @@
use std::{
convert::TryInto,
future::Future,
io::Write,
pin::Pin,
process::{Command, Stdio},
sync::{Arc, Mutex},
@ -40,9 +41,11 @@ use crate::{conf::accounts::JobRequest, jobs::JoinHandle, terminal::embed::Embed
#[cfg(feature = "gpgme")]
mod gpg;
mod edit_attachments;
pub mod edit_attachments;
use edit_attachments::*;
pub mod hooks;
#[derive(Debug, PartialEq, Eq)]
enum Cursor {
Headers,
@ -67,19 +70,18 @@ impl EmbedStatus {
impl std::ops::Deref for EmbedStatus {
type Target = Arc<Mutex<EmbedTerminal>>;
fn deref(&self) -> &Arc<Mutex<EmbedTerminal>> {
use EmbedStatus::*;
fn deref(&self) -> &Self::Target {
match self {
Stopped(ref e, _) | Running(ref e, _) => e,
Self::Stopped(ref e, _) | Self::Running(ref e, _) => e,
}
}
}
impl std::ops::DerefMut for EmbedStatus {
fn deref_mut(&mut self) -> &mut Arc<Mutex<EmbedTerminal>> {
use EmbedStatus::*;
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Stopped(ref mut e, _) | Running(ref mut e, _) => e,
Self::Stopped(ref mut e, _) | Self::Running(ref mut e, _) => e,
}
}
}
@ -104,6 +106,7 @@ pub struct Composer {
dirty: bool,
has_changes: bool,
initialized: bool,
hooks: Vec<hooks::Hook>,
id: ComponentId,
}
@ -156,6 +159,12 @@ impl Composer {
cursor: Cursor::Headers,
pager,
draft: Draft::default(),
hooks: vec![
hooks::HEADERWARN,
hooks::PASTDATEWARN,
hooks::MISSINGATTACHMENTWARN,
hooks::EMPTYDRAFTWARN,
],
form: FormWidget::default(),
mode: ViewMode::Edit,
#[cfg(feature = "gpgme")]
@ -174,6 +183,11 @@ impl Composer {
account_hash,
..Composer::new(context)
};
ret.hooks.retain(|h| {
!account_settings!(context[account_hash].composing.disabled_compose_hooks)
.iter()
.any(|hn| hn.as_str() == h.name())
});
for (h, v) in
account_settings!(context[account_hash].composing.default_header_values).iter()
{
@ -202,6 +216,11 @@ impl Composer {
context: &Context,
) -> Result<Self> {
let mut ret = Composer::with_account(account_hash, context);
ret.hooks.retain(|h| {
!account_settings!(context[account_hash].composing.disabled_compose_hooks)
.iter()
.any(|hn| hn.as_str() == h.name())
});
let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash);
ret.draft = Draft::edit(&envelope, bytes)?;
@ -211,13 +230,18 @@ impl Composer {
}
pub fn reply_to(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
reply_to_all: bool,
) -> Self {
let mut ret = Composer::with_account(coordinates.0, context);
let account = &context.accounts[&coordinates.0];
let mut ret = Composer::with_account(account_hash, context);
ret.hooks.retain(|h| {
!account_settings!(context[account_hash].composing.disabled_compose_hooks)
.iter()
.any(|hn| hn.as_str() == h.name())
});
let account = &context.accounts[&account_hash];
let envelope = account.collection.get_env(coordinates.2);
let subject = {
let subject = envelope.subject();
@ -267,7 +291,7 @@ impl Composer {
ret.draft
.set_header("In-Reply-To", envelope.message_id_display().into());
if let Some(reply_to) = envelope.other_headers().get("To").map(|v| v.as_str()) {
if let Some(reply_to) = envelope.other_headers().get("To") {
let to: &str = reply_to;
let extra_identities = &account.settings.account.extra_identities;
if let Some(extra) = extra_identities
@ -299,13 +323,13 @@ impl Composer {
if let Some(reply_to) = envelope
.other_headers()
.get("Mail-Followup-To")
.and_then(|v| v.as_str().try_into().ok())
.and_then(|v| v.try_into().ok())
{
to.insert(reply_to);
} else if let Some(reply_to) = envelope
.other_headers()
.get("Reply-To")
.and_then(|v| v.as_str().try_into().ok())
.and_then(|v| v.try_into().ok())
{
to.insert(reply_to);
} else {
@ -368,12 +392,12 @@ impl Composer {
}
pub fn reply_to_select(
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String,
context: &mut Context,
) -> Self {
let mut ret = Composer::reply_to(coordinates, reply_body, context, false);
let account = &context.accounts[&coordinates.0];
let account = &context.accounts[&account_hash];
let parent_message = account.collection.get_env(coordinates.2);
/* If message is from a mailing list and we detect a List-Post header, ask
* user if they want to reply to the mailing list or the submitter of
@ -496,6 +520,15 @@ To: {}
}
}
fn run_hooks(&mut self, ctx: &mut Context) -> Result<()> {
for h in self.hooks.iter_mut() {
if let err @ Err(_) = h(ctx, &mut self.draft) {
return err;
}
}
Ok(())
}
fn update_form(&mut self) {
let old_cursor = self.form.cursor();
self.form = FormWidget::new(("Save".into(), true));
@ -1408,6 +1441,11 @@ impl Component for Composer {
&& self.mode.is_edit() =>
{
self.update_draft();
if let Err(err) = self.run_hooks(context) {
context
.replies
.push_back(UIEvent::Notification(None, err.to_string(), None));
}
self.mode = ViewMode::Send(UIConfirmationDialog::new(
"send mail?",
vec![(true, "yes".to_string()), (false, "no".to_string())],
@ -1434,7 +1472,6 @@ impl Component for Composer {
self.set_dirty(true);
}
UIEvent::EmbedInput((ref k, ref b)) => {
use std::io::Write;
if let Some(ref mut embed) = self.embed {
let mut embed_guard = embed.lock().unwrap();
if embed_guard.write_all(b).is_err() {
@ -2390,8 +2427,11 @@ fn attribution_string(
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore]
fn test_compose_reply_subject_prefix() {
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
@ -2403,8 +2443,10 @@ Content-Type: text/plain
hello world.
"#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let mut context = Context::new_mock();
let envelope =
Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let tempdir = tempfile::tempdir().unwrap();
let mut context = Context::new_mock(&tempdir);
let account_hash = context.accounts[0].hash();
let mailbox_hash = MailboxHash::default();
let envelope_hash = envelope.hash();
@ -2431,7 +2473,8 @@ Content-Type: text/plain
hello world.
"#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let envelope =
Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let envelope_hash = envelope.hash();
context.accounts[0]
.collection
@ -2448,3 +2491,4 @@ hello world.
r#"some name <some@example.com>"#
);
}
}

View File

@ -0,0 +1,354 @@
/*
* meli
*
* Copyright 2023 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! Pre-submission hooks for draft validation and/or transformations.
pub use std::borrow::Cow;
use super::*;
/*
pub enum HookFn {
/// Stateful hook.
Closure(Box<dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync>),
/// Static hook.
Ptr(fn(&mut Context, &mut Draft) -> Result<()>),
}
*/
/// Pre-submission hook for draft validation and/or transformations.
pub struct Hook {
/// Hook name for enabling/disabling it from configuration.
///
/// See [`ComposingSettings::disabled_compose_hooks`].
name: Cow<'static, str>,
hook_fn: fn(&mut Context, &mut Draft) -> Result<()>,
//hook_fn: HookFn,
}
impl std::fmt::Debug for Hook {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct(self.name.as_ref()).finish()
}
}
impl Hook {
/// Hook name for enabling/disabling it from configuration.
///
/// See [`ComposingSettings::disabled_compose_hooks`].
pub fn name(&self) -> &str {
self.name.as_ref()
}
}
impl std::ops::Deref for Hook {
type Target = dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync;
fn deref(&self) -> &Self::Target {
&self.hook_fn
}
}
impl std::ops::DerefMut for Hook {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.hook_fn
}
}
fn past_date_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
use melib::datetime::*;
if let Some(v) = draft
.headers
.get("Date")
.map(rfc822_to_timestamp)
.and_then(Result::ok)
{
let now: UnixTimestamp = now();
let diff = now.abs_diff(v);
if diff >= 60 * 60 {
return Err(format!(
"Value of Date header is {} minutes in the {}.",
diff / 60,
if now > v { "past" } else { "future" }
)
.into());
}
}
Ok(())
}
/// Warn if [`melib::Draft`] Date is far in the past/future.
pub const PASTDATEWARN: Hook = Hook {
name: Cow::Borrowed("past-date-warn"),
hook_fn: past_date_warn,
};
fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
for hdr in ["From", "To"] {
match draft.headers.get(hdr).map(melib::Address::list_try_from) {
Some(Ok(_)) => {}
Some(Err(err)) => return Err(format!("{hdr} header value is invalid ({err}).").into()),
None => return Err(format!("{hdr} header is missing and should be present.").into()),
}
}
{
match draft
.headers
.get("Date")
.map(melib::datetime::rfc822_to_timestamp)
{
Some(Err(err)) => return Err(format!("Date header value is invalid ({err}).").into()),
Some(Ok(0)) => return Err("Date header value is invalid.".into()),
_ => {}
}
}
for hdr in ["Cc", "Bcc"] {
if let Some(Err(err)) = draft
.headers
.get(hdr)
.filter(|v| !v.trim().is_empty())
.map(melib::Address::list_try_from)
{
return Err(format!("{hdr} header value is invalid ({err}).").into());
}
}
Ok(())
}
/// Warn if important [`melib::Draft`] header is missing or invalid.
pub const HEADERWARN: Hook = Hook {
name: Cow::Borrowed("important-header-warn"),
hook_fn: important_header_warn,
};
fn missing_attachment_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
if draft
.headers
.get("Subject")
.map(|s| s.to_lowercase().contains("attach"))
.unwrap_or(false)
&& draft.attachments.is_empty()
{
return Err("Subject mentions attachments but attachments are empty.".into());
}
if draft.body.to_lowercase().contains("attach") && draft.attachments.is_empty() {
return Err("Draft body mentions attachments but attachments are empty.".into());
}
Ok(())
}
/// Warn if Subject and/or draft body mentions attachments but they are missing.
pub const MISSINGATTACHMENTWARN: Hook = Hook {
name: Cow::Borrowed("missing-attachment-warn"),
hook_fn: missing_attachment_warn,
};
fn empty_draft_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
if draft
.headers
.get("Subject")
.filter(|v| !v.trim().is_empty())
.is_none()
&& draft.body.trim().is_empty()
{
return Err("Both Subject and body are empty.".into());
}
Ok(())
}
/// Warn if draft has no subject and no body.
pub const EMPTYDRAFTWARN: Hook = Hook {
name: Cow::Borrowed("empty-draft-warn"),
hook_fn: empty_draft_warn,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_draft_hook_datewarn() {
let tempdir = tempfile::tempdir().unwrap();
let mut ctx = Context::new_mock(&tempdir);
let mut draft = Draft::default();
draft
.set_body("αδφαφσαφασ".to_string())
.set_header("Subject", "test_update()".into())
.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
println!("Check that past Date header value produces a warning…");
#[allow(const_item_mutation)]
let err_msg = PASTDATEWARN(&mut ctx, &mut draft).unwrap_err().to_string();
assert!(
err_msg.starts_with("Value of Date header is "),
"PASTDATEWARN should complain about Date value being in the past: {}",
err_msg
);
assert!(
err_msg.ends_with(" minutes in the past."),
"PASTDATEWARN should complain about Date value being in the past: {}",
err_msg
);
}
#[test]
fn test_draft_hook_headerwarn() {
let tempdir = tempfile::tempdir().unwrap();
let mut ctx = Context::new_mock(&tempdir);
let mut draft = Draft::default();
draft
.set_body("αδφαφσαφασ".to_string())
.set_header("Subject", "test_update()".into())
.set_header("Date", "Sun sds16 Jun 2013 17:56:45 +0200".into());
let mut hook = HEADERWARN;
println!("Check for missing/empty From header value…");
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
assert_eq!(
err_msg,
"From header value is invalid (Parsing error. In input: \"...\",\nError: Alternative, \
Many1, Alternative, atom(): starts with whitespace or empty).",
"HEADERWARN should complain about From value being empty: {}",
err_msg
);
draft.set_header("From", "user <user@example.com>".into());
println!("Check for missing/empty To header value…");
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
assert_eq!(
err_msg,
"To header value is invalid (Parsing error. In input: \"...\",\nError: Alternative, \
Many1, Alternative, atom(): starts with whitespace or empty).",
"HEADERWARN should complain about To value being empty: {}",
err_msg
);
draft.set_header("To", "other user <user@example.com>".into());
println!("Check for invalid Date header value…");
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
assert_eq!(
err_msg, "Date header value is invalid.",
"HEADERWARN should complain about Date value being invalid: {}",
err_msg
);
println!("Check that valid header values produces no errors…");
draft = Draft::default();
draft
.set_body("αδφαφσαφασ".to_string())
.set_header("From", "user <user@example.com>".into())
.set_header("To", "other user <user@example.com>".into())
.set_header("Subject", "test_update()".into());
hook(&mut ctx, &mut draft).unwrap();
draft.set_header("From", "user user@example.com>".into());
println!("Check for invalid From header value…");
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
assert_eq!(
err_msg,
"From header value is invalid (Parsing error. In input: \"user \
user@example.com>...\",\nError: Alternative, Tag).",
"HEADERWARN should complain about From value being invalid: {}",
err_msg
);
}
#[test]
fn test_draft_hook_missingattachmentwarn() {
let tempdir = tempfile::tempdir().unwrap();
let mut ctx = Context::new_mock(&tempdir);
let mut draft = Draft::default();
draft
.set_body("αδφαφσαφασ".to_string())
.set_header("Subject", "Attachments included".into())
.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
let mut hook = MISSINGATTACHMENTWARN;
println!(
"Check that mentioning attachments in Subject produces a warning if draft has no \
attachments"
);
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
assert_eq!(
err_msg, "Subject mentions attachments but attachments are empty.",
"MISSINGATTACHMENTWARN should complain about missing attachments: {}",
err_msg
);
draft
.set_header("Subject", "Hello.".into())
.set_body("Attachments included".to_string());
println!(
"Check that mentioning attachments in body produces a warning if draft has no \
attachments"
);
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
assert_eq!(
err_msg, "Draft body mentions attachments but attachments are empty.",
"MISSINGATTACHMENTWARN should complain about missing attachments: {}",
err_msg
);
println!(
"Check that mentioning attachments produces no warnings if draft has attachments…"
);
draft.set_header("Subject", "Attachments included".into());
let mut attachment = AttachmentBuilder::new(b"");
attachment
.set_raw(b"foobar".to_vec())
.set_content_type(ContentType::Other {
name: Some("info.txt".to_string()),
tag: b"text/plain".to_vec(),
parameters: vec![],
})
.set_content_transfer_encoding(ContentTransferEncoding::Base64);
draft.attachments_mut().push(attachment);
hook(&mut ctx, &mut draft).unwrap();
}
#[test]
fn test_draft_hook_emptydraftwarn() {
let tempdir = tempfile::tempdir().unwrap();
let mut ctx = Context::new_mock(&tempdir);
let mut draft = Draft::default();
draft.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
let mut hook = EMPTYDRAFTWARN;
println!("Check that empty draft produces a warning…");
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
assert_eq!(
err_msg, "Both Subject and body are empty.",
"EMPTYDRAFTWARN should complain about empty draft: {}",
err_msg
);
println!("Check that non-empty draft produces no warning…");
draft.set_header("Subject", "Ping".into());
hook(&mut ctx, &mut draft).unwrap();
}
}

View File

@ -374,12 +374,13 @@ macro_rules! column_str {
impl Deref for $name {
type Target = String;
fn deref(&self) -> &String {
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for $name {
fn deref_mut(&mut self) -> &mut String {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -100,6 +100,9 @@ pub struct ComposingSettings {
/// The prefix to use in reply subjects. The de facto prefix is "Re:".
#[serde(default = "res", alias = "reply-prefix")]
pub reply_prefix: String,
/// Disabled `compose-hooks`.
#[serde(default, alias = "disabled-compose-hooks")]
pub disabled_compose_hooks: Vec<String>,
}
impl Default for ComposingSettings {
@ -118,6 +121,7 @@ impl Default for ComposingSettings {
forward_as_attachment: ToggleFlag::Ask,
reply_prefix_list_to_strip: None,
reply_prefix: res(),
disabled_compose_hooks: vec![],
}
}
}

View File

@ -25,460 +25,19 @@
//! This module is automatically generated by config_macros.rs.
use super::*;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct PagerSettingsOverride {
#[doc = " Number of context lines when going to next page."]
#[doc = " Default: 0"]
#[serde(alias = "pager-context")]
#[serde(default)]
pub pager_context: Option<usize>,
#[doc = " Stop at the end instead of displaying next mail."]
#[doc = " Default: false"]
#[serde(alias = "pager-stop")]
#[serde(default)]
pub pager_stop: Option<bool>,
#[doc = " Always show headers when scrolling."]
#[doc = " Default: true"]
#[serde(alias = "headers-sticky")]
#[serde(default)]
pub headers_sticky: Option<bool>,
#[doc = " The height of the pager in mail view, in percent."]
#[doc = " Default: 80"]
#[serde(alias = "pager-ratio")]
#[serde(default)]
pub pager_ratio: Option<usize>,
#[doc = " A command to pipe mail output through for viewing in pager."]
#[doc = " Default: None"]
#[serde(deserialize_with = "non_empty_string")]
#[serde(default)]
pub filter: Option<Option<String>>,
#[doc = " A command to pipe html output before displaying it in a pager"]
#[doc = " Default: None"]
#[serde(deserialize_with = "non_empty_string", alias = "html-filter")]
#[serde(default)]
pub html_filter: Option<Option<String>>,
#[doc = " Respect \"format=flowed\""]
#[doc = " Default: true"]
#[serde(alias = "format-flowed")]
#[serde(default)]
pub format_flowed: Option<bool>,
#[doc = " Split long lines that would overflow on the x axis."]
#[doc = " Default: true"]
#[serde(alias = "split-long-lines")]
#[serde(default)]
pub split_long_lines: Option<bool>,
#[doc = " Minimum text width in columns."]
#[doc = " Default: 80"]
#[serde(alias = "minimum-width")]
#[serde(default)]
pub minimum_width: Option<usize>,
#[doc = " Choose `text/html` alternative if `text/plain` is empty in `multipart/alternative`"]
#[doc = " attachments."]
#[doc = " Default: true"]
#[serde(alias = "auto-choose-multipart-alternative")]
#[serde(default)]
pub auto_choose_multipart_alternative: Option<ToggleFlag>,
#[doc = " Show Date: in my timezone"]
#[doc = " Default: true"]
#[serde(alias = "show-date-in-my-timezone")]
#[serde(default)]
pub show_date_in_my_timezone: Option<ToggleFlag>,
#[doc = " A command to launch URLs with. The URL will be given as the first argument of the \
command."]
#[doc = " Default: None"]
#[serde(deserialize_with = "non_empty_string")]
#[serde(default)]
pub url_launcher: Option<Option<String>>,
#[doc = " A command to open html files."]
#[doc = " Default: None"]
#[serde(deserialize_with = "non_empty_string", alias = "html-open")]
#[serde(default)]
pub html_open: Option<Option<String>>,
}
impl Default for PagerSettingsOverride {
fn default() -> Self {
PagerSettingsOverride {
pager_context: None,
pager_stop: None,
headers_sticky: None,
pager_ratio: None,
filter: None,
html_filter: None,
format_flowed: None,
split_long_lines: None,
minimum_width: None,
auto_choose_multipart_alternative: None,
show_date_in_my_timezone: None,
url_launcher: None,
html_open: None,
}
}
}
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "headers-sticky")] # [serde (default)] pub headers_sticky : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { PagerSettingsOverride { pager_context : None , pager_stop : None , headers_sticky : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None } } }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct ListingSettingsOverride {
#[doc = " Number of context lines when going to next page."]
#[doc = " Default: 0"]
#[serde(alias = "context-lines")]
#[serde(default)]
pub context_lines: Option<usize>,
#[doc = "Show auto-hiding scrollbar in accounts sidebar menu."]
#[doc = "Default: True"]
#[serde(default)]
pub show_menu_scrollbar: Option<bool>,
#[doc = " Datetime formatting passed verbatim to strftime(3)."]
#[doc = " Default: %Y-%m-%d %T"]
#[serde(alias = "datetime-fmt")]
#[serde(default)]
pub datetime_fmt: Option<Option<String>>,
#[doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."]
#[doc = " Default: true"]
#[serde(alias = "recent-dates")]
#[serde(default)]
pub recent_dates: Option<bool>,
#[doc = " Show only envelopes that match this query"]
#[doc = " Default: None"]
#[serde(default)]
pub filter: Option<Option<Query>>,
#[serde(alias = "index-style")]
#[serde(default)]
pub index_style: Option<IndexStyle>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_has_sibling: Option<Option<String>>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_no_sibling: Option<Option<String>>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_has_sibling_leaf: Option<Option<String>>,
#[doc = "Default: \" \""]
#[serde(default)]
pub sidebar_mailbox_tree_no_sibling_leaf: Option<Option<String>>,
#[doc = "Default: ' '"]
#[serde(default)]
pub sidebar_divider: Option<char>,
#[doc = "Default: 90"]
#[serde(default)]
pub sidebar_ratio: Option<usize>,
#[doc = " Flag to show if thread entry contains unseen mail."]
#[doc = " Default: \"\""]
#[serde(default)]
pub unseen_flag: Option<Option<String>>,
#[doc = " Flag to show if thread has been snoozed."]
#[doc = " Default: \"💤\""]
#[serde(default)]
pub thread_snoozed_flag: Option<Option<String>>,
#[doc = " Flag to show if thread entry has been selected."]
#[doc = " Default: \"\u{fe0f}\""]
#[serde(default)]
pub selected_flag: Option<Option<String>>,
#[doc = " Flag to show if thread entry contains attachments."]
#[doc = " Default: \"📎\""]
#[serde(default)]
pub attachment_flag: Option<Option<String>>,
#[doc = " Should threads with differentiating Subjects show a list of those subjects on the \
entry"]
#[doc = " title?"]
#[doc = " Default: \"true\""]
#[serde(default)]
pub thread_subject_pack: Option<bool>,
}
impl Default for ListingSettingsOverride {
fn default() -> Self {
ListingSettingsOverride {
context_lines: None,
show_menu_scrollbar: None,
datetime_fmt: None,
recent_dates: None,
filter: None,
index_style: None,
sidebar_mailbox_tree_has_sibling: None,
sidebar_mailbox_tree_no_sibling: None,
sidebar_mailbox_tree_has_sibling_leaf: None,
sidebar_mailbox_tree_no_sibling_leaf: None,
sidebar_divider: None,
sidebar_ratio: None,
unseen_flag: None,
thread_snoozed_flag: None,
selected_flag: None,
attachment_flag: None,
thread_subject_pack: None,
}
}
}
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = "Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = "Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = "Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = "Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = "Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Should threads with differentiating Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { ListingSettingsOverride { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , thread_subject_pack : None } } }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct NotificationsSettingsOverride {
#[doc = " Enable notifications."]
#[doc = " Default: True"]
#[serde(default)]
pub enable: Option<bool>,
#[doc = " A command to pipe notifications through."]
#[doc = " Default: None"]
#[serde(default)]
pub script: Option<Option<String>>,
#[doc = " A command to pipe new mail notifications through (preferred over `script`)."]
#[doc = " Default: None"]
#[serde(default)]
pub new_mail_script: Option<Option<String>>,
#[doc = " A file location which has its size changed when new mail arrives (max 128 bytes). \
Can be"]
#[doc = " used to trigger new mail notifications eg with `xbiff(1)`."]
#[doc = " Default: None"]
#[serde(alias = "xbiff-file-path")]
#[serde(default)]
pub xbiff_file_path: Option<Option<String>>,
#[serde(alias = "play-sound")]
#[serde(default)]
pub play_sound: Option<ToggleFlag>,
#[serde(alias = "sound-file")]
#[serde(default)]
pub sound_file: Option<Option<String>>,
}
impl Default for NotificationsSettingsOverride {
fn default() -> Self {
NotificationsSettingsOverride {
enable: None,
script: None,
new_mail_script: None,
xbiff_file_path: None,
play_sound: None,
sound_file: None,
}
}
}
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct NotificationsSettingsOverride { # [doc = " Enable notifications."] # [doc = " Default: True"] # [serde (default)] pub enable : Option < bool > , # [doc = " A command to pipe notifications through."] # [doc = " Default: None"] # [serde (default)] pub script : Option < Option < String > > , # [doc = " A command to pipe new mail notifications through (preferred over"] # [doc = " `script`). Default: None"] # [serde (default)] pub new_mail_script : Option < Option < String > > , # [doc = " A file location which has its size changed when new mail arrives (max"] # [doc = " 128 bytes). Can be used to trigger new mail notifications eg with"] # [doc = " `xbiff(1)`. Default: None"] # [serde (alias = "xbiff-file-path")] # [serde (default)] pub xbiff_file_path : Option < Option < String > > , # [serde (alias = "play-sound")] # [serde (default)] pub play_sound : Option < ToggleFlag > , # [serde (alias = "sound-file")] # [serde (default)] pub sound_file : Option < Option < String > > } impl Default for NotificationsSettingsOverride { fn default () -> Self { NotificationsSettingsOverride { enable : None , script : None , new_mail_script : None , xbiff_file_path : None , play_sound : None , sound_file : None } } }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct ShortcutsOverride {
#[serde(default)]
pub general: Option<GeneralShortcuts>,
#[serde(default)]
pub listing: Option<ListingShortcuts>,
#[serde(default)]
pub composing: Option<ComposingShortcuts>,
#[serde(alias = "contact-list")]
#[serde(default)]
pub contact_list: Option<ContactListShortcuts>,
#[serde(alias = "envelope-view")]
#[serde(default)]
pub envelope_view: Option<EnvelopeViewShortcuts>,
#[serde(alias = "thread-view")]
#[serde(default)]
pub thread_view: Option<ThreadViewShortcuts>,
#[serde(default)]
pub pager: Option<PagerShortcuts>,
}
impl Default for ShortcutsOverride {
fn default() -> Self {
ShortcutsOverride {
general: None,
listing: None,
composing: None,
contact_list: None,
envelope_view: None,
thread_view: None,
pager: None,
}
}
}
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { ShortcutsOverride { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct ComposingSettingsOverride {
#[doc = " A command to pipe new emails to"]
#[doc = " Required"]
#[serde(default)]
pub send_mail: Option<SendMail>,
#[doc = " Command to launch editor. Can have arguments. Draft filename is given as the last \
argument. If it's missing, the environment variable $EDITOR is looked up."]
#[serde(alias = "editor-command", alias = "editor-cmd", alias = "editor_cmd")]
#[serde(default)]
pub editor_command: Option<Option<String>>,
#[doc = " Embed editor (for terminal interfaces) instead of forking and waiting."]
#[serde(default)]
pub embed: Option<bool>,
#[doc = " Set \"format=flowed\" in plain text attachments."]
#[doc = " Default: true"]
#[serde(alias = "format-flowed")]
#[serde(default)]
pub format_flowed: Option<bool>,
#[doc = "Set User-Agent"]
#[doc = "Default: empty"]
#[serde(alias = "insert_user_agent")]
#[serde(default)]
pub insert_user_agent: Option<bool>,
#[doc = " Set default header values for new drafts"]
#[doc = " Default: empty"]
#[serde(alias = "default-header-values")]
#[serde(default)]
pub default_header_values: Option<HashMap<String, String>>,
#[doc = " Wrap header preample when editing a draft in an editor. This allows you to write \
non-plain"]
#[doc = " text email without the preamble creating syntax errors. They are stripped when you \
return"]
#[doc = " from the editor. The values should be a two element array of strings, a prefix and \
suffix."]
#[doc = " Default: None"]
#[serde(alias = "wrap-header-preample")]
#[serde(default)]
pub wrap_header_preamble: Option<Option<(String, String)>>,
#[doc = " Store sent mail after successful submission. This setting is meant to be disabled \
for"]
#[doc = " non-standard behaviour in gmail, which auto-saves sent mail on its own."]
#[doc = " Default: true"]
#[serde(default)]
pub store_sent_mail: Option<bool>,
#[doc = " The attribution line appears above the quoted reply text."]
#[doc = " The format specifiers for the replied address are:"]
#[doc = " - `%+f` — the sender's name and email address."]
#[doc = " - `%+n` — the sender's name (or email address, if no name is included)."]
#[doc = " - `%+a` — the sender's email address."]
#[doc = " The format string is passed to strftime(3) with the replied envelope's date."]
#[doc = " Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""]
#[serde(default)]
pub attribution_format_string: Option<Option<String>>,
#[doc = " Whether the strftime call for the attribution string uses the POSIX locale instead \
of"]
#[doc = " the user's active locale"]
#[doc = " Default: true"]
#[serde(default)]
pub attribution_use_posix_locale: Option<bool>,
#[doc = " Forward emails as attachment? (Alternative is inline)"]
#[doc = " Default: ask"]
#[serde(alias = "forward-as-attachment")]
#[serde(default)]
pub forward_as_attachment: Option<ToggleFlag>,
#[doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"]
#[doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \
\"Sv:\", \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \
\"TR:\", \"AW:\", \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \
\"ΣΧΕΤ:\", \"Σχετ:\", \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \
\"Továbbítás:\", \"R:\", \"I:\", \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \
\"VB:\", \"RV:\", \"RES:\", \"Res\", \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \
\"İLT:\", \"ATB:\", \"YML:\"]`"]
#[serde(alias = "reply-prefix-list-to-strip")]
#[serde(default)]
pub reply_prefix_list_to_strip: Option<Option<Vec<String>>>,
#[doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."]
#[serde(alias = "reply-prefix")]
#[serde(default)]
pub reply_prefix: Option<String>,
}
impl Default for ComposingSettingsOverride {
fn default() -> Self {
ComposingSettingsOverride {
send_mail: None,
editor_command: None,
embed: None,
format_flowed: None,
insert_user_agent: None,
default_header_values: None,
wrap_header_preamble: None,
store_sent_mail: None,
attribution_format_string: None,
attribution_use_posix_locale: None,
forward_as_attachment: None,
reply_prefix_list_to_strip: None,
reply_prefix: None,
}
}
}
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] # [serde (default)] pub embed : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = "Set User-Agent"] # [doc = "Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < String , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embed : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , disabled_compose_hooks : None } } }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct TagsSettingsOverride {
#[serde(deserialize_with = "tag_color_de")]
#[serde(default)]
pub colors: Option<HashMap<TagHash, Color>>,
#[serde(deserialize_with = "tag_set_de", alias = "ignore-tags")]
#[serde(default)]
pub ignore_tags: Option<HashSet<TagHash>>,
}
impl Default for TagsSettingsOverride {
fn default() -> Self {
TagsSettingsOverride {
colors: None,
ignore_tags: None,
}
}
}
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { TagsSettingsOverride { colors : None , ignore_tags : None } } }
#[cfg(feature = "gpgme")]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct PGPSettingsOverride {
#[doc = " auto verify signed e-mail according to RFC3156"]
#[doc = " Default: true"]
#[serde(alias = "auto-verify-signatures")]
#[serde(default)]
pub auto_verify_signatures: Option<bool>,
#[doc = " auto decrypt encrypted e-mail"]
#[doc = " Default: true"]
#[serde(alias = "auto-decrypt")]
#[serde(default)]
pub auto_decrypt: Option<bool>,
#[doc = " always sign sent e-mail"]
#[doc = " Default: false"]
#[serde(alias = "auto-sign")]
#[serde(default)]
pub auto_sign: Option<bool>,
#[doc = " Auto encrypt sent e-mail"]
#[doc = " Default: false"]
#[serde(alias = "auto-encrypt")]
#[serde(default)]
pub auto_encrypt: Option<bool>,
#[doc = " Default: None"]
#[serde(alias = "sign-key")]
#[serde(default)]
pub sign_key: Option<Option<String>>,
#[doc = " Default: None"]
#[serde(alias = "decrypt-key")]
#[serde(default)]
pub decrypt_key: Option<Option<String>>,
#[doc = " Default: None"]
#[serde(alias = "encrypt-key")]
#[serde(default)]
pub encrypt_key: Option<Option<String>>,
#[doc = " Allow remote lookups"]
#[doc = " Default: None"]
#[serde(alias = "allow-remote-lookups")]
#[serde(default)]
pub allow_remote_lookup: Option<ToggleFlag>,
#[doc = " Remote lookup mechanisms."]
#[doc = " Default: \"local,wkd\""]
#[serde(alias = "remote-lookup-mechanisms")]
#[serde(default)]
pub remote_lookup_mechanisms: Option<melib::gpgme::LocateKey>,
}
#[cfg(feature = "gpgme")]
impl Default for PGPSettingsOverride {
fn default() -> Self {
PGPSettingsOverride {
auto_verify_signatures: None,
auto_decrypt: None,
auto_sign: None,
auto_encrypt: None,
sign_key: None,
decrypt_key: None,
encrypt_key: None,
allow_remote_lookup: None,
remote_lookup_mechanisms: None,
}
}
}
# [cfg (feature = "gpgme")] # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { # [doc = " auto verify signed e-mail according to RFC3156"] # [doc = " Default: true"] # [serde (alias = "auto-verify-signatures")] # [serde (default)] pub auto_verify_signatures : Option < bool > , # [doc = " auto decrypt encrypted e-mail"] # [doc = " Default: true"] # [serde (alias = "auto-decrypt")] # [serde (default)] pub auto_decrypt : Option < bool > , # [doc = " always sign sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-sign")] # [serde (default)] pub auto_sign : Option < bool > , # [doc = " Auto encrypt sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-encrypt")] # [serde (default)] pub auto_encrypt : Option < bool > , # [doc = " Default: None"] # [serde (alias = "sign-key")] # [serde (default)] pub sign_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "decrypt-key")] # [serde (default)] pub decrypt_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "encrypt-key")] # [serde (default)] pub encrypt_key : Option < Option < String > > , # [doc = " Allow remote lookups"] # [doc = " Default: None"] # [serde (alias = "allow-remote-lookups")] # [serde (default)] pub allow_remote_lookup : Option < ToggleFlag > , # [doc = " Remote lookup mechanisms."] # [doc = " Default: \"local,wkd\""] # [serde (alias = "remote-lookup-mechanisms")] # [serde (default)] pub remote_lookup_mechanisms : Option < melib :: gpgme :: LocateKey > } # [cfg (feature = "gpgme")] impl Default for PGPSettingsOverride { fn default () -> Self { PGPSettingsOverride { auto_verify_signatures : None , auto_decrypt : None , auto_sign : None , auto_encrypt : None , sign_key : None , decrypt_key : None , encrypt_key : None , allow_remote_lookup : None , remote_lookup_mechanisms : None } } }
# [cfg (not (feature = "gpgme"))] # [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { } # [cfg (not (feature = "gpgme"))] impl Default for PGPSettingsOverride { fn default () -> Self { PGPSettingsOverride { } } }
#[cfg(not(feature = "gpgme"))]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct PGPSettingsOverride {}
#[cfg(not(feature = "gpgme"))]
impl Default for PGPSettingsOverride {
fn default() -> Self {
PGPSettingsOverride {}
}
}

View File

@ -30,7 +30,12 @@
//!
//! On startup a [DFS](https://en.wikipedia.org/wiki/Depth-first_search) is performed to see if there are any cycles in the link graph.
use std::{borrow::Cow, collections::HashSet, fmt::Write};
use std::{
borrow::Cow,
collections::HashSet,
fmt::Write,
ops::{Deref, DerefMut},
};
use indexmap::IndexMap;
use melib::{Error, Result};
@ -740,16 +745,16 @@ mod regexp {
}
}
use std::ops::{Deref, DerefMut};
impl Deref for Theme {
type Target = IndexMap<Cow<'static, str>, ThemeAttributeInner>;
fn deref(&self) -> &Self::Target {
&self.keys
}
}
impl DerefMut for Theme {
fn deref_mut(&mut self) -> &mut IndexMap<Cow<'static, str>, ThemeAttributeInner> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.keys
}
}

View File

@ -162,7 +162,7 @@ impl Context {
}
#[cfg(test)]
pub fn new_mock() -> Self {
pub fn new_mock(dir: &tempfile::TempDir) -> Self {
let (sender, receiver) =
crossbeam::channel::bounded(32 * ::std::mem::size_of::<ThreadEvent>());
let job_executor = Arc::new(JobExecutor::new(sender.clone()));
@ -177,7 +177,7 @@ impl Context {
let mut account_conf = AccountConf::default();
account_conf.conf.format = "maildir".to_string();
account_conf.account.format = "maildir".to_string();
account_conf.account.root_mailbox = "/tmp/".to_string();
account_conf.account.root_mailbox = dir.path().display().to_string();
let sender = sender.clone();
let account_hash = AccountHash::from_bytes(name.as_bytes());
Account::new(

View File

@ -438,13 +438,13 @@ impl CellBuffer {
impl Deref for CellBuffer {
type Target = [Cell];
fn deref(&self) -> &[Cell] {
fn deref(&self) -> &Self::Target {
&self.buf
}
}
impl DerefMut for CellBuffer {
fn deref_mut(&mut self) -> &mut [Cell] {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.buf
}
}

View File

@ -67,19 +67,18 @@ impl EmbedStatus {
impl std::ops::Deref for EmbedStatus {
type Target = Arc<Mutex<EmbedTerminal>>;
fn deref(&self) -> &Arc<Mutex<EmbedTerminal>> {
use EmbedStatus::*;
fn deref(&self) -> &Self::Target {
match self {
Stopped(ref e) | Running(ref e) => e,
Self::Stopped(ref e) | Self::Running(ref e) => e,
}
}
}
impl std::ops::DerefMut for EmbedStatus {
fn deref_mut(&mut self) -> &mut Arc<Mutex<EmbedTerminal>> {
use EmbedStatus::*;
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Stopped(ref mut e) | Running(ref mut e) => e,
Self::Stopped(ref mut e) | Self::Running(ref mut e) => e,
}
}
}