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
parent
1f1ea30769
commit
8c671935f9
|
@ -1155,6 +1155,7 @@ dependencies = [
|
|||
"structopt",
|
||||
"svg",
|
||||
"syn",
|
||||
"tempfile",
|
||||
"termion",
|
||||
"toml",
|
||||
"unicode-segmentation",
|
||||
|
|
|
@ -66,6 +66,7 @@ syn = { version = "1.0.92", features = [] }
|
|||
|
||||
[dev-dependencies]
|
||||
regex = "1"
|
||||
tempfile = "3.3"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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()));
|
||||
log::set_boxed_logger(Box::new(logger.clone())).unwrap();
|
||||
|
||||
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 */
|
||||
|
|
|
@ -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,10 +2427,13 @@ fn attribution_string(
|
|||
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_compose_reply_subject_prefix() {
|
||||
let raw_mail = r#"From: "some name" <some@example.com>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compose_reply_subject_prefix() {
|
||||
let raw_mail = r#"From: "some name" <some@example.com>
|
||||
To: "me" <myself@example.com>
|
||||
Cc:
|
||||
Subject: RE: your e-mail
|
||||
|
@ -2403,26 +2443,28 @@ 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 account_hash = context.accounts[0].hash();
|
||||
let mailbox_hash = MailboxHash::default();
|
||||
let envelope_hash = envelope.hash();
|
||||
context.accounts[0]
|
||||
.collection
|
||||
.insert(envelope, mailbox_hash);
|
||||
let composer = Composer::reply_to(
|
||||
(account_hash, mailbox_hash, envelope_hash),
|
||||
String::new(),
|
||||
&mut context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail");
|
||||
assert_eq!(
|
||||
&composer.draft.headers()["To"],
|
||||
r#"some name <some@example.com>"#
|
||||
);
|
||||
let raw_mail = r#"From: "some name" <some@example.com>
|
||||
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();
|
||||
context.accounts[0]
|
||||
.collection
|
||||
.insert(envelope, mailbox_hash);
|
||||
let composer = Composer::reply_to(
|
||||
(account_hash, mailbox_hash, envelope_hash),
|
||||
String::new(),
|
||||
&mut context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail");
|
||||
assert_eq!(
|
||||
&composer.draft.headers()["To"],
|
||||
r#"some name <some@example.com>"#
|
||||
);
|
||||
let raw_mail = r#"From: "some name" <some@example.com>
|
||||
To: "me" <myself@example.com>
|
||||
Cc:
|
||||
Subject: your e-mail
|
||||
|
@ -2431,20 +2473,22 @@ Content-Type: text/plain
|
|||
|
||||
hello world.
|
||||
"#;
|
||||
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
let envelope_hash = envelope.hash();
|
||||
context.accounts[0]
|
||||
.collection
|
||||
.insert(envelope, mailbox_hash);
|
||||
let composer = Composer::reply_to(
|
||||
(account_hash, mailbox_hash, envelope_hash),
|
||||
String::new(),
|
||||
&mut context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail");
|
||||
assert_eq!(
|
||||
&composer.draft.headers()["To"],
|
||||
r#"some name <some@example.com>"#
|
||||
);
|
||||
let envelope =
|
||||
Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
let envelope_hash = envelope.hash();
|
||||
context.accounts[0]
|
||||
.collection
|
||||
.insert(envelope, mailbox_hash);
|
||||
let composer = Composer::reply_to(
|
||||
(account_hash, mailbox_hash, envelope_hash),
|
||||
String::new(),
|
||||
&mut context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail");
|
||||
assert_eq!(
|
||||
&composer.draft.headers()["To"],
|
||||
r#"some name <some@example.com>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ,< |