forked from meli/meli
1
Fork 0

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.
duesee/experiment/use_imap_codec
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", "structopt",
"svg", "svg",
"syn", "syn",
"tempfile",
"termion", "termion",
"toml", "toml",
"unicode-segmentation", "unicode-segmentation",

View File

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

View File

@ -127,6 +127,7 @@ theme = "light"
Available options are listed below. Available options are listed below.
Default values are shown in parentheses. Default values are shown in parentheses.
.Sh ACCOUNTS .Sh ACCOUNTS
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic root_mailbox Ar String .It Ic root_mailbox Ar String
The backend-specific path of the root_mailbox, usually INBOX. 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" .Pq Em "utf7", "utf-7", "utf8", "utf-8"
.El .El
.Sh COMPOSING .Sh COMPOSING
Composing specific options Composing specific options.
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic send_mail Ar String|SmtpServerConf .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. 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:" the Latin "res", in the matter of) followed by the contents of the "Subject:"
field body of the original message. field body of the original message.
.Ed .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 .El
.Sh SHORTCUTS .Sh SHORTCUTS
Default values are shown in parentheses.
Shortcuts can take the following values: Shortcuts can take the following values:
.Bl -bullet -compact .Bl -bullet -compact
.It .It
@ -1040,6 +1064,7 @@ toggle thread view visibility
.El .El
.sp .sp
.Sh NOTIFICATIONS .Sh NOTIFICATIONS
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic enable Ar boolean .It Ic enable Ar boolean
Enable notifications. Enable notifications.
@ -1074,6 +1099,7 @@ Play sound file in notifications if possible.
.Pq Em none .Pq Em none
.El .El
.Sh PAGER .Sh PAGER
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic headers_sticky Ar boolean .It Ic headers_sticky Ar boolean
.Pq Em optional .Pq Em optional
@ -1128,6 +1154,7 @@ The URL will be given as the first argument of the command.
.Pq Em xdg-open .Pq Em xdg-open
.El .El
.Sh LISTING .Sh LISTING
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic show_menu_scrollbar Ar boolean .It Ic show_menu_scrollbar Ar boolean
.Pq Em optional .Pq Em optional
@ -1269,6 +1296,7 @@ no_sibling_leaf = " \\_"
.Ed .Ed
.sp .sp
.Sh TAGS .Sh TAGS
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic colours Ar hash table String[Color] .It Ic colours Ar hash table String[Color]
.Pq Em optional .Pq Em optional
@ -1291,6 +1319,7 @@ colors = { signed="#Ff6600", replied="DeepSkyBlue4", draft="#f00", replied="8" }
"INBOX" = { tags.ignore_tags=["inbox", ] } "INBOX" = { tags.ignore_tags=["inbox", ] }
.Ed .Ed
.Sh PGP .Sh PGP
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic auto_verify_signatures Ar boolean .It Ic auto_verify_signatures Ar boolean
Auto verify signed e-mail according to RFC3156 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 .Pq Em none
.El .El
.Sh TERMINAL .Sh TERMINAL
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic theme Ar String .It Ic theme Ar String
.Pq Em optional .Pq Em optional
@ -1431,6 +1461,7 @@ progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] }
.Ed .Ed
.El .El
.Sh LOG .Sh LOG
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic log_file Ar String .It Ic log_file Ar String
.Pq Em optional .Pq Em optional
@ -1467,6 +1498,7 @@ to
.Pq Em INFO .Pq Em INFO
.El .El
.Sh SMTP Connections .Sh SMTP Connections
Default values are shown in parentheses.
.Bl -tag -width 36n .Bl -tag -width 36n
.It Ic hostname Ar String .It Ic hostname Ar String
server hostname server hostname

View File

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

View File

@ -444,7 +444,6 @@ impl Envelope {
if self.bcc.is_empty() { if self.bcc.is_empty() {
self.other_headers self.other_headers
.get("Bcc") .get("Bcc")
.map(|s| s.as_str())
.unwrap_or_default() .unwrap_or_default()
.to_string() .to_string()
} else { } else {
@ -460,11 +459,7 @@ impl Envelope {
pub fn field_cc_to_string(&self) -> String { pub fn field_cc_to_string(&self) -> String {
if self.cc.is_empty() { if self.cc.is_empty() {
self.other_headers self.other_headers.get("Cc").unwrap_or_default().to_string()
.get("Cc")
.map(|s| s.as_str())
.unwrap_or_default()
.to_string()
} else { } else {
self.cc.iter().fold(String::new(), |mut acc, x| { self.cc.iter().fold(String::new(), |mut acc, x| {
if !acc.is_empty() { if !acc.is_empty() {
@ -480,7 +475,6 @@ impl Envelope {
if self.from.is_empty() { if self.from.is_empty() {
self.other_headers self.other_headers
.get("From") .get("From")
.map(|s| s.as_str())
.unwrap_or_default() .unwrap_or_default()
.to_string() .to_string()
} else { } else {
@ -508,11 +502,7 @@ impl Envelope {
pub fn field_to_to_string(&self) -> String { pub fn field_to_to_string(&self) -> String {
if self.to.is_empty() { if self.to.is_empty() {
self.other_headers self.other_headers.get("To").unwrap_or_default().to_string()
.get("To")
.map(|s| s.as_str())
.unwrap_or_default()
.to_string()
} else { } else {
self.to self.to
.iter() .iter()
@ -532,7 +522,6 @@ impl Envelope {
if refs.is_empty() { if refs.is_empty() {
self.other_headers self.other_headers
.get("References") .get("References")
.map(|s| s.as_str())
.unwrap_or_default() .unwrap_or_default()
.to_string() .to_string()
} else { } 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)] #[cfg(test)]
mod tests { mod tests {
use std::str::FromStr; 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) (self.0).get_mut(HeaderNameType(key).borrow() as &dyn HeaderKey)
} }
pub fn get(&self, key: &str) -> Option<&String> { pub fn get(&self, key: &str) -> Option<&str> {
(self.0).get(HeaderNameType(key).borrow() as &dyn HeaderKey) (self.0)
.get(HeaderNameType(key).borrow() as &dyn HeaderKey)
.map(|x| x.as_str())
} }
pub fn contains_key(&self, key: &str) -> bool { pub fn contains_key(&self, key: &str) -> bool {
@ -274,7 +276,7 @@ impl Deref for HeaderMap {
} }
impl DerefMut 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 &mut self.0
} }
} }

View File

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

View File

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

View File

@ -19,10 +19,10 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
#[cfg(not(test))]
use std::{fs::OpenOptions, path::PathBuf};
use std::{ use std::{
fs::OpenOptions,
io::{BufWriter, Write}, io::{BufWriter, Write},
path::PathBuf,
sync::{ sync::{
atomic::{AtomicU8, Ordering}, atomic::{AtomicU8, Ordering},
Arc, Mutex, Arc, Mutex,
@ -31,8 +31,6 @@ use std::{
use log::{Level, LevelFilter, Log, Metadata, Record}; use log::{Level, LevelFilter, Log, Metadata, Record};
use crate::shellexpand::ShellExpandTrait;
#[derive(Copy, Clone, Default, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Default, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
#[repr(u8)] #[repr(u8)]
pub enum LogLevel { pub enum LogLevel {
@ -137,6 +135,9 @@ pub enum Destination {
#[derive(Clone)] #[derive(Clone)]
pub struct StderrLogger { pub struct StderrLogger {
#[cfg(test)]
dest: Arc<Mutex<BufWriter<std::io::Stderr>>>,
#[cfg(not(test))]
dest: Arc<Mutex<BufWriter<std::fs::File>>>, dest: Arc<Mutex<BufWriter<std::fs::File>>>,
level: Arc<AtomicU8>, level: Arc<AtomicU8>,
print_level: bool, print_level: bool,
@ -163,6 +164,11 @@ impl Default for StderrLogger {
impl StderrLogger { impl StderrLogger {
pub fn new(level: LogLevel) -> Self { pub fn new(level: LogLevel) -> Self {
use std::sync::Once;
static INIT_STDERR_LOGGING: Once = Once::new();
#[cfg(not(test))]
let logger = { let logger = {
let data_dir = xdg::BaseDirectories::with_prefix("meli").unwrap(); 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 */ 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, 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")] #[cfg(feature = "debug-tracing")]
log::set_max_level( log::set_max_level(
@ -191,7 +207,10 @@ impl StderrLogger {
); );
#[cfg(not(feature = "debug-tracing"))] #[cfg(not(feature = "debug-tracing"))]
log::set_max_level(LevelFilter::from(logger.log_level())); log::set_max_level(LevelFilter::from(logger.log_level()));
log::set_boxed_logger(Box::new(logger.clone())).unwrap();
INIT_STDERR_LOGGING.call_once(|| {
log::set_boxed_logger(Box::new(logger.clone())).unwrap();
});
logger logger
} }
@ -199,7 +218,10 @@ impl StderrLogger {
self.level.load(Ordering::SeqCst).into() self.level.load(Ordering::SeqCst).into()
} }
#[cfg(not(test))]
pub fn change_log_dest(&mut self, path: PathBuf) { pub fn change_log_dest(&mut self, path: PathBuf) {
use crate::shellexpand::ShellExpandTrait;
let path = path.expand(); // expand shell stuff let path = path.expand(); // expand shell stuff
let mut dest = self.dest.lock().unwrap(); 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 */ *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::{ use std::{
convert::TryInto, convert::TryInto,
future::Future, future::Future,
io::Write,
pin::Pin, pin::Pin,
process::{Command, Stdio}, process::{Command, Stdio},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@ -40,9 +41,11 @@ use crate::{conf::accounts::JobRequest, jobs::JoinHandle, terminal::embed::Embed
#[cfg(feature = "gpgme")] #[cfg(feature = "gpgme")]
mod gpg; mod gpg;
mod edit_attachments; pub mod edit_attachments;
use edit_attachments::*; use edit_attachments::*;
pub mod hooks;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
enum Cursor { enum Cursor {
Headers, Headers,
@ -67,19 +70,18 @@ impl EmbedStatus {
impl std::ops::Deref for EmbedStatus { impl std::ops::Deref for EmbedStatus {
type Target = Arc<Mutex<EmbedTerminal>>; type Target = Arc<Mutex<EmbedTerminal>>;
fn deref(&self) -> &Arc<Mutex<EmbedTerminal>> {
use EmbedStatus::*; fn deref(&self) -> &Self::Target {
match self { match self {
Stopped(ref e, _) | Running(ref e, _) => e, Self::Stopped(ref e, _) | Self::Running(ref e, _) => e,
} }
} }
} }
impl std::ops::DerefMut for EmbedStatus { impl std::ops::DerefMut for EmbedStatus {
fn deref_mut(&mut self) -> &mut Arc<Mutex<EmbedTerminal>> { fn deref_mut(&mut self) -> &mut Self::Target {
use EmbedStatus::*;
match self { 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, dirty: bool,
has_changes: bool, has_changes: bool,
initialized: bool, initialized: bool,
hooks: Vec<hooks::Hook>,
id: ComponentId, id: ComponentId,
} }
@ -156,6 +159,12 @@ impl Composer {
cursor: Cursor::Headers, cursor: Cursor::Headers,
pager, pager,
draft: Draft::default(), draft: Draft::default(),
hooks: vec![
hooks::HEADERWARN,
hooks::PASTDATEWARN,
hooks::MISSINGATTACHMENTWARN,
hooks::EMPTYDRAFTWARN,
],
form: FormWidget::default(), form: FormWidget::default(),
mode: ViewMode::Edit, mode: ViewMode::Edit,
#[cfg(feature = "gpgme")] #[cfg(feature = "gpgme")]
@ -174,6 +183,11 @@ impl Composer {
account_hash, account_hash,
..Composer::new(context) ..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 for (h, v) in
account_settings!(context[account_hash].composing.default_header_values).iter() account_settings!(context[account_hash].composing.default_header_values).iter()
{ {
@ -202,6 +216,11 @@ impl Composer {
context: &Context, context: &Context,
) -> Result<Self> { ) -> Result<Self> {
let mut ret = Composer::with_account(account_hash, context); 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); let envelope: EnvelopeRef = context.accounts[&account_hash].collection.get_env(env_hash);
ret.draft = Draft::edit(&envelope, bytes)?; ret.draft = Draft::edit(&envelope, bytes)?;
@ -211,13 +230,18 @@ impl Composer {
} }
pub fn reply_to( pub fn reply_to(
coordinates: (AccountHash, MailboxHash, EnvelopeHash), coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String, reply_body: String,
context: &mut Context, context: &mut Context,
reply_to_all: bool, reply_to_all: bool,
) -> Self { ) -> Self {
let mut ret = Composer::with_account(coordinates.0, context); let mut ret = Composer::with_account(account_hash, context);
let account = &context.accounts[&coordinates.0]; 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 envelope = account.collection.get_env(coordinates.2);
let subject = { let subject = {
let subject = envelope.subject(); let subject = envelope.subject();
@ -267,7 +291,7 @@ impl Composer {
ret.draft ret.draft
.set_header("In-Reply-To", envelope.message_id_display().into()); .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 to: &str = reply_to;
let extra_identities = &account.settings.account.extra_identities; let extra_identities = &account.settings.account.extra_identities;
if let Some(extra) = extra_identities if let Some(extra) = extra_identities
@ -299,13 +323,13 @@ impl Composer {
if let Some(reply_to) = envelope if let Some(reply_to) = envelope
.other_headers() .other_headers()
.get("Mail-Followup-To") .get("Mail-Followup-To")
.and_then(|v| v.as_str().try_into().ok()) .and_then(|v| v.try_into().ok())
{ {
to.insert(reply_to); to.insert(reply_to);
} else if let Some(reply_to) = envelope } else if let Some(reply_to) = envelope
.other_headers() .other_headers()
.get("Reply-To") .get("Reply-To")
.and_then(|v| v.as_str().try_into().ok()) .and_then(|v| v.try_into().ok())
{ {
to.insert(reply_to); to.insert(reply_to);
} else { } else {
@ -368,12 +392,12 @@ impl Composer {
} }
pub fn reply_to_select( pub fn reply_to_select(
coordinates: (AccountHash, MailboxHash, EnvelopeHash), coordinates @ (account_hash, _, _): (AccountHash, MailboxHash, EnvelopeHash),
reply_body: String, reply_body: String,
context: &mut Context, context: &mut Context,
) -> Self { ) -> Self {
let mut ret = Composer::reply_to(coordinates, reply_body, context, false); 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); let parent_message = account.collection.get_env(coordinates.2);
/* If message is from a mailing list and we detect a List-Post header, ask /* 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 * 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) { fn update_form(&mut self) {
let old_cursor = self.form.cursor(); let old_cursor = self.form.cursor();
self.form = FormWidget::new(("Save".into(), true)); self.form = FormWidget::new(("Save".into(), true));
@ -1408,6 +1441,11 @@ impl Component for Composer {
&& self.mode.is_edit() => && self.mode.is_edit() =>
{ {
self.update_draft(); 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( self.mode = ViewMode::Send(UIConfirmationDialog::new(
"send mail?", "send mail?",
vec![(true, "yes".to_string()), (false, "no".to_string())], vec![(true, "yes".to_string()), (false, "no".to_string())],
@ -1434,7 +1472,6 @@ impl Component for Composer {
self.set_dirty(true); self.set_dirty(true);
} }
UIEvent::EmbedInput((ref k, ref b)) => { UIEvent::EmbedInput((ref k, ref b)) => {
use std::io::Write;
if let Some(ref mut embed) = self.embed { if let Some(ref mut embed) = self.embed {
let mut embed_guard = embed.lock().unwrap(); let mut embed_guard = embed.lock().unwrap();
if embed_guard.write_all(b).is_err() { if embed_guard.write_all(b).is_err() {
@ -2390,10 +2427,13 @@ fn attribution_string(
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix) melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
} }
#[test] #[cfg(test)]
#[ignore] mod tests {
fn test_compose_reply_subject_prefix() { use super::*;
let raw_mail = r#"From: "some name" <some@example.com>
#[test]
fn test_compose_reply_subject_prefix() {
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com> To: "me" <myself@example.com>
Cc: Cc:
Subject: RE: your e-mail Subject: RE: your e-mail
@ -2403,26 +2443,28 @@ Content-Type: text/plain
hello world. hello world.
"#; "#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail"); let envelope =
let mut context = Context::new_mock(); Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let account_hash = context.accounts[0].hash(); let tempdir = tempfile::tempdir().unwrap();
let mailbox_hash = MailboxHash::default(); let mut context = Context::new_mock(&tempdir);
let envelope_hash = envelope.hash(); let account_hash = context.accounts[0].hash();
context.accounts[0] let mailbox_hash = MailboxHash::default();
.collection let envelope_hash = envelope.hash();
.insert(envelope, mailbox_hash); context.accounts[0]
let composer = Composer::reply_to( .collection
(account_hash, mailbox_hash, envelope_hash), .insert(envelope, mailbox_hash);
String::new(), let composer = Composer::reply_to(
&mut context, (account_hash, mailbox_hash, envelope_hash),
false, String::new(),
); &mut context,
assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail"); false,
assert_eq!( );
&composer.draft.headers()["To"], assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail");
r#"some name <some@example.com>"# assert_eq!(
); &composer.draft.headers()["To"],
let raw_mail = r#"From: "some name" <some@example.com> r#"some name <some@example.com>"#
);
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com> To: "me" <myself@example.com>
Cc: Cc:
Subject: your e-mail Subject: your e-mail
@ -2431,20 +2473,22 @@ Content-Type: text/plain
hello world. hello world.
"#; "#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail"); let envelope =
let envelope_hash = envelope.hash(); Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
context.accounts[0] let envelope_hash = envelope.hash();
.collection context.accounts[0]
.insert(envelope, mailbox_hash); .collection
let composer = Composer::reply_to( .insert(envelope, mailbox_hash);
(account_hash, mailbox_hash, envelope_hash), let composer = Composer::reply_to(
String::new(), (account_hash, mailbox_hash, envelope_hash),
&mut context, String::new(),
false, &mut context,
); false,
assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail"); );
assert_eq!( assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail");
&composer.draft.headers()["To"], assert_eq!(
r#"some name <some@example.com>"# &composer.draft.headers()["To"],
); 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 { impl Deref for $name {
type Target = String; type Target = String;
fn deref(&self) -> &String {
fn deref(&self) -> &Self::Target {
&self.0 &self.0
} }
} }
impl DerefMut for $name { impl DerefMut for $name {
fn deref_mut(&mut self) -> &mut String { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0 &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:". /// The prefix to use in reply subjects. The de facto prefix is "Re:".
#[serde(default = "res", alias = "reply-prefix")] #[serde(default = "res", alias = "reply-prefix")]
pub reply_prefix: String, pub reply_prefix: String,
/// Disabled `compose-hooks`.
#[serde(default, alias = "disabled-compose-hooks")]
pub disabled_compose_hooks: Vec<String>,
} }
impl Default for ComposingSettings { impl Default for ComposingSettings {
@ -118,6 +121,7 @@ impl Default for ComposingSettings {
forward_as_attachment: ToggleFlag::Ask, forward_as_attachment: ToggleFlag::Ask,
reply_prefix_list_to_strip: None, reply_prefix_list_to_strip: None,
reply_prefix: res(), reply_prefix: res(),
disabled_compose_hooks: vec![],
} }
} }
} }

View File

@ -25,460 +25,19 @@
//! This module is automatically generated by config_macros.rs. //! This module is automatically generated by config_macros.rs.
use super::*; use super::*;
#[derive(Debug, Serialize, Deserialize, Clone)] # [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 } } }
#[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)] # [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 } } }
#[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)] # [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 } } }
#[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)] # [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 } } }
#[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)] # [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 } } }
#[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)] # [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 } } }
#[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")] # [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 } } }
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)] # [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 { } } }
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 {}
}
}

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. //! 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 indexmap::IndexMap;
use melib::{Error, Result}; use melib::{Error, Result};
@ -740,16 +745,16 @@ mod regexp {
} }
} }
use std::ops::{Deref, DerefMut};
impl Deref for Theme { impl Deref for Theme {
type Target = IndexMap<Cow<'static, str>, ThemeAttributeInner>; type Target = IndexMap<Cow<'static, str>, ThemeAttributeInner>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.keys &self.keys
} }
} }
impl DerefMut for Theme { 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 &mut self.keys
} }
} }

View File

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

View File

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

View File

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