Compare commits

..

3 Commits

Author SHA1 Message Date
Manos Pitsidianakis 792fcee954 Add notification history view 2022-10-16 20:21:38 +03:00
Manos Pitsidianakis 4085622a1c components.rs: add perform() method in Component trait 2022-10-16 20:21:38 +03:00
Manos Pitsidianakis 96f9aa8072 conf/shortcuts.rs: add key_slice() method to shortcut structs
Add a new method that returns a static slice of included shortcuts.
2022-10-16 20:21:38 +03:00
39 changed files with 1305 additions and 1491 deletions

View File

@ -21,9 +21,9 @@ path = "src/main.rs"
name = "meli"
path = "src/lib.rs"
[[bin]]
name = "managesieve-client"
path = "src/managesieve.rs"
#[[bin]]
#name = "managesieve-meli"
#path = "src/managesieve.rs"
#[[bin]]
#name = "async"

View File

@ -101,12 +101,12 @@ Custom themes can be included in your configuration files or be saved independen
directory as TOML files.
To start creating a theme right away, you can begin by editing the default theme keys and values:
.sp
.Dl meli print-default-theme > ~/.config/meli/themes/new_theme.toml
.Dl meli --print-default-theme > ~/.config/meli/themes/new_theme.toml
.sp
.Pa new_theme.toml
will now include all keys and values of the "dark" theme.
.sp
.Dl meli print-loaded-themes
.Dl meli --print-loaded-themes
.sp
will print all loaded themes with the links resolved.
.Sh VALID ATTRIBUTE VALUES

View File

@ -21,22 +21,16 @@
use super::{ImapConnection, ImapProtocol, ImapServerConf, UIDStore};
use crate::conf::AccountSettings;
use crate::email::parser::IResult;
use crate::error::{MeliError, Result};
use crate::get_conf_val;
use crate::imap::RequiredResponses;
use nom::{
branch::alt, bytes::complete::tag, combinator::map, multi::separated_list1,
sequence::separated_pair,
branch::alt, bytes::complete::tag, combinator::map, error::Error as NomError, error::ErrorKind,
multi::separated_list1, sequence::separated_pair, IResult,
};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
pub struct ManageSieveConnection {
pub inner: ImapConnection,
}
pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
let (_, ret) = separated_list1(
tag(b"\r\n"),
@ -48,225 +42,26 @@ pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
Ok(ret)
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum ManageSieveResponse<'a> {
Ok {
code: Option<&'a [u8]>,
message: Option<&'a [u8]>,
},
NoBye {
code: Option<&'a [u8]>,
message: Option<&'a [u8]>,
},
}
#[test]
fn test_managesieve_capabilities() {
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
(&b"NOTIFY"[..],&b"mailto"[..]),
(&b"SASL"[..],&b"PLAIN"[..]),
(&b"STARTTLS"[..], &b""[..]),
(&b"VERSION"[..],&b"1.0"[..])]
mod parser {
use super::*;
use nom::bytes::complete::tag;
pub use nom::bytes::complete::{is_not, tag_no_case};
use nom::character::complete::crlf;
use nom::combinator::{iterator, map, opt};
pub use nom::sequence::{delimited, pair, preceded, terminated};
pub fn sieve_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
crate::backends::imap::protocol_parser::string_token(input)
}
// *(sieve-name [SP "ACTIVE"] CRLF)
// response-oknobye
pub fn listscripts(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], bool)>> {
let mut it = iterator(
input,
alt((
terminated(
map(terminated(sieve_name, tag_no_case(b" ACTIVE")), |r| {
(r, true)
}),
crlf,
),
terminated(map(sieve_name, |r| (r, false)), crlf),
)),
);
let parsed = (&mut it).collect::<Vec<(&[u8], bool)>>();
let res: IResult<_, _> = it.finish();
let (rest, _) = res?;
Ok((rest, parsed))
}
// response-getscript = (sieve-script CRLF response-ok) /
// response-nobye
pub fn getscript(input: &[u8]) -> IResult<&[u8], &[u8]> {
sieve_name(input)
}
pub fn response_oknobye(input: &[u8]) -> IResult<&[u8], ManageSieveResponse> {
alt((
map(
terminated(
pair(
preceded(
tag_no_case(b"ok"),
opt(preceded(
tag(b" "),
delimited(tag(b"("), is_not(")"), tag(b")")),
)),
),
opt(preceded(tag(b" "), sieve_name)),
),
crlf,
),
|(code, message)| ManageSieveResponse::Ok { code, message },
),
map(
terminated(
pair(
preceded(
alt((tag_no_case(b"no"), tag_no_case(b"bye"))),
opt(preceded(
tag(b" "),
delimited(tag(b"("), is_not(")"), tag(b")")),
)),
),
opt(preceded(tag(b" "), sieve_name)),
),
crlf,
),
|(code, message)| ManageSieveResponse::NoBye { code, message },
),
))(input)
}
#[test]
fn test_managesieve_listscripts() {
let input_1 = b"\"summer_script\"\r\n\"vacation_script\"\r\n{13}\r\nclever\"script\r\n\"main_script\" ACTIVE\r\nOK";
assert_eq!(
terminated(listscripts, tag_no_case(b"OK"))(input_1),
Ok((
&b""[..],
vec![
(&b"summer_script"[..], false),
(&b"vacation_script"[..], false),
(&b"clever\"script"[..], false),
(&b"main_script"[..], true)
]
))
);
let input_2 = b"\"summer_script\"\r\n\"main_script\" active\r\nok";
assert_eq!(
terminated(listscripts, tag_no_case(b"OK"))(input_2),
Ok((
&b""[..],
vec![(&b"summer_script"[..], false), (&b"main_script"[..], true)]
))
);
let input_3 = b"ok";
assert_eq!(
terminated(listscripts, tag_no_case(b"OK"))(input_3),
Ok((&b""[..], vec![]))
);
}
#[test]
fn test_managesieve_general() {
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
(&b"NOTIFY"[..],&b"mailto"[..]),
(&b"SASL"[..],&b"PLAIN"[..]),
(&b"STARTTLS"[..], &b""[..]),
(&b"VERSION"[..],&b"1.0"[..])]
);
let response_ok = b"OK (WARNINGS) \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: Some(&b"WARNINGS"[..]),
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
}
))
);
let response_ok = b"OK (WARNINGS)\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: Some(&b"WARNINGS"[..]),
message: None,
}
))
);
let response_ok =
b"OK \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: None,
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
}
))
);
let response_ok = b"Ok\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: None,
message: None,
}
))
);
let response_nobye = b"No (NONEXISTENT) \"There is no script by that name\"\r\n";
assert_eq!(
response_oknobye(response_nobye),
Ok((
&b""[..],
ManageSieveResponse::NoBye {
code: Some(&b"NONEXISTENT"[..]),
message: Some(&b"There is no script by that name"[..]),
}
))
);
let response_nobye = b"No (NONEXISTENT) {31}\r\nThere is no script by that name\r\n";
assert_eq!(
response_oknobye(response_nobye),
Ok((
&b""[..],
ManageSieveResponse::NoBye {
code: Some(&b"NONEXISTENT"[..]),
message: Some(&b"There is no script by that name"[..]),
}
))
);
let response_nobye = b"No\r\n";
assert_eq!(
response_oknobye(response_nobye),
Ok((
&b""[..],
ManageSieveResponse::NoBye {
code: None,
message: None,
}
))
);
}
);
}
// Return a byte sequence surrounded by "s and decoded if necessary
pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
if input.is_empty() || input[0] != b'"' {
return Err(nom::Err::Error((input, "empty").into()));
return Err(nom::Err::Error(NomError {
input,
code: ErrorKind::Tag,
}));
}
let mut i = 1;
@ -277,199 +72,91 @@ pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
i += 1;
}
Err(nom::Err::Error((input, "no quotes").into()))
Err(nom::Err::Error(NomError {
input,
code: ErrorKind::Tag,
}))
}
impl ManageSieveConnection {
pub fn new(
account_hash: crate::backends::AccountHash,
account_name: String,
s: &AccountSettings,
event_consumer: crate::backends::BackendEventConsumer,
) -> Result<Self> {
let server_hostname = get_conf_val!(s["server_hostname"])?;
let server_username = get_conf_val!(s["server_username"])?;
let server_password = get_conf_val!(s["server_password"])?;
let server_port = get_conf_val!(s["server_port"], 4190)?;
let danger_accept_invalid_certs: bool =
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
let timeout = get_conf_val!(s["timeout"], 16_u64)?;
let timeout = if timeout == 0 {
None
} else {
Some(std::time::Duration::from_secs(timeout))
};
let server_conf = ImapServerConf {
server_hostname: server_hostname.to_string(),
server_username: server_username.to_string(),
server_password: server_password.to_string(),
server_port,
use_starttls: true,
use_tls: true,
danger_accept_invalid_certs,
protocol: ImapProtocol::ManageSieve,
timeout,
};
let uid_store = Arc::new(UIDStore {
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(MeliError::new("Account is uninitialised.")),
))),
..UIDStore::new(
account_hash,
Arc::new(account_name),
event_consumer,
server_conf.timeout,
)
});
Ok(Self {
inner: ImapConnection::new_connection(&server_conf, uid_store),
})
}
pub trait ManageSieve {
fn havespace(&mut self) -> Result<()>;
fn putscript(&mut self) -> Result<()>;
pub async fn havespace(&mut self) -> Result<()> {
fn listscripts(&mut self) -> Result<()>;
fn setactive(&mut self) -> Result<()>;
fn getscript(&mut self) -> Result<()>;
fn deletescript(&mut self) -> Result<()>;
fn renamescript(&mut self) -> Result<()>;
}
pub fn new_managesieve_connection(
account_hash: crate::backends::AccountHash,
account_name: String,
s: &AccountSettings,
event_consumer: crate::backends::BackendEventConsumer,
) -> Result<ImapConnection> {
let server_hostname = get_conf_val!(s["server_hostname"])?;
let server_username = get_conf_val!(s["server_username"])?;
let server_password = get_conf_val!(s["server_password"])?;
let server_port = get_conf_val!(s["server_port"], 4190)?;
let danger_accept_invalid_certs: bool = get_conf_val!(s["danger_accept_invalid_certs"], false)?;
let timeout = get_conf_val!(s["timeout"], 16_u64)?;
let timeout = if timeout == 0 {
None
} else {
Some(std::time::Duration::from_secs(timeout))
};
let server_conf = ImapServerConf {
server_hostname: server_hostname.to_string(),
server_username: server_username.to_string(),
server_password: server_password.to_string(),
server_port,
use_starttls: true,
use_tls: true,
danger_accept_invalid_certs,
protocol: ImapProtocol::ManageSieve,
timeout,
};
let uid_store = Arc::new(UIDStore {
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(MeliError::new("Account is uninitialised.")),
))),
..UIDStore::new(
account_hash,
Arc::new(account_name),
event_consumer,
server_conf.timeout,
)
});
Ok(ImapConnection::new_connection(&server_conf, uid_store))
}
impl ManageSieve for ImapConnection {
fn havespace(&mut self) -> Result<()> {
Ok(())
}
fn putscript(&mut self) -> Result<()> {
Ok(())
}
pub async fn putscript(&mut self, script_name: &[u8], script: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Putscript {{{len}+}}\r\n", len = script_name.len()).as_bytes())
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.send_literal(format!(" {{{len}+}}\r\n", len = script.len()).as_bytes())
.await?;
self.inner.send_literal(script).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Could not upload script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
fn listscripts(&mut self) -> Result<()> {
Ok(())
}
fn setactive(&mut self) -> Result<()> {
Ok(())
}
pub async fn listscripts(&mut self) -> Result<Vec<(Vec<u8>, bool)>> {
let mut ret = Vec::new();
self.inner.send_command(b"Listscripts").await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, scripts) =
parser::terminated(parser::listscripts, parser::tag_no_case(b"OK"))(&ret)?;
Ok(scripts
.into_iter()
.map(|(n, a)| (n.to_vec(), a))
.collect::<Vec<(Vec<u8>, bool)>>())
fn getscript(&mut self) -> Result<()> {
Ok(())
}
pub async fn checkscript(&mut self, script: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Checkscript {{{len}+}}\r\n", len = script.len()).as_bytes())
.await?;
self.inner.send_literal(script).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Checkscript reply: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
fn deletescript(&mut self) -> Result<()> {
Ok(())
}
pub async fn setactive(&mut self, script_name: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Setactive {{{len}+}}\r\n", len = script_name.len()).as_bytes())
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Could not set active script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
}
pub async fn getscript(&mut self, script_name: &[u8]) -> Result<Vec<u8>> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Getscript {{{len}+}}\r\n", len = script_name.len()).as_bytes())
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
if let Ok((_, ManageSieveResponse::NoBye { code, message })) =
parser::response_oknobye(&ret)
{
return Err(format!(
"Could not set active script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into());
}
let (_rest, script) =
parser::terminated(parser::getscript, parser::tag_no_case(b"OK"))(&ret)?;
Ok(script.to_vec())
}
pub async fn deletescript(&mut self, script_name: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(
format!("Deletescript {{{len}+}}\r\n", len = script_name.len()).as_bytes(),
)
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Could not delete script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
}
pub async fn renamescript(&mut self) -> Result<()> {
fn renamescript(&mut self) -> Result<()> {
Ok(())
}
}

View File

@ -150,9 +150,8 @@ fn test_imap_required_responses() {
assert_eq!(v.len(), 1);
}
#[derive(Debug, PartialEq)]
#[derive(Debug)]
pub struct Alert(String);
pub type ImapParseResult<'a, T> = Result<(&'a [u8], T, Option<Alert>)>;
pub struct ImapLineIterator<'a> {
slice: &'a [u8],
@ -605,8 +604,8 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += (input.len() - i - rest.len()) + 1;
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: {:.40}.",
String::from_utf8_lossy(&input[i..])
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} else if input[i..].starts_with(b"MODSEQ (") {
@ -640,8 +639,8 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {:.40}",
String::from_utf8_lossy(&input[i..])
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} else if input[i..].starts_with(b"ENVELOPE (") {
@ -651,7 +650,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: {:.40}",
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
@ -673,7 +672,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (REFERENCES)]: {:.40}",
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
@ -689,7 +688,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (\"REFERENCES\"): {:.40}",
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
@ -737,9 +736,9 @@ pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult<Vec<FetchResponse<'_
}
Err(err) => {
return Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH responses: {} `{:.40}`",
err,
"Unexpected input while parsing UID FETCH responses: `{:.40}`, {}",
String::from_utf8_lossy(input),
err
)));
}
}
@ -935,7 +934,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
}
#[test]
fn test_imap_untagged_responses() {
fn test_untagged_responses() {
use UntaggedResponse::*;
assert_eq!(
untagged_responses(b"* 2 EXISTS\r\n")
@ -978,37 +977,6 @@ fn test_imap_untagged_responses() {
);
}
#[test]
fn test_imap_fetch_response() {
let input: &[u8] = b"* 198 FETCH (UID 7608 FLAGS (\\Seen) ENVELOPE (\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\") (\"'xx'\" NIL \"xx.xx\" \"xx.com\") (\"'xx xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\") BODY[HEADER.FIELDS (REFERENCES)] {2}\r\n\r\nBODYSTRUCTURE ((\"text\" \"html\" (\"charset\" \"us-ascii\") \"<xx@xx>\" NIL \"7BIT\" 17236 232 NIL NIL NIL NIL)(\"image\" \"jpeg\" (\"name\" \"image001.jpg\") \"<image001.jpg@xx.xx>\" \"image001.jpg\" \"base64\" 1918 NIL (\"inline\" (\"filename\" \"image001.jpg\" \"size\" \"1650\" \"creation-date\" \"Sun, 09 Aug 2015 20:56:04 GMT\" \"modification-date\" \"Sun, 14 Aug 2022 22:11:45 GMT\")) NIL NIL) \"related\" (\"boundary\" \"xx--xx\" \"type\" \"text/html\") NIL \"en-US\"))\r\n";
let mut address = SmallVec::new();
address.push(Address::new(None, "xx@xx.com".to_string()));
let mut env = Envelope::new(0);
env.set_subject("xxxx/xxxx".as_bytes().to_vec());
env.set_date("Fri, 24 Jun 2011 10:09:10 +0000".as_bytes());
env.set_from(address.clone());
env.set_to(address);
env.set_message_id("<xx@xx.com>".as_bytes());
assert_eq!(
fetch_response(input).unwrap(),
(
&b""[..],
FetchResponse {
uid: Some(7608),
message_sequence_number: 198,
flags: Some((Flag::SEEN, vec![])),
modseq: None,
body: None,
references: None,
envelope: Some(env),
raw_fetch_value: input,
},
None
)
);
}
pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec<ImapNum>> {
alt((
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
@ -1159,7 +1127,7 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
}
#[test]
fn test_imap_select_response() {
fn test_select_response() {
let r = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.\r\n* 45 EXISTS\r\n* 0 RECENT\r\n* OK [UNSEEN 16] First unseen.\r\n* OK [UIDVALIDITY 1554422056] UIDs valid\r\n* OK [UIDNEXT 50] Predicted next UID\r\n";
assert_eq!(
@ -1379,12 +1347,6 @@ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
))
}
#[test]
fn test_imap_envelope() {
let input: &[u8] = b"(\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\") (\"'xx'\" NIL \"xx.xx\" \"xx.com\") (\"'xx xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\")";
_ = envelope(input).unwrap();
}
/* Helper to build StrBuilder for Address structs */
macro_rules! str_builder {
($offset:expr, $length:expr) => {
@ -1404,7 +1366,7 @@ pub fn envelope_addresses<'a>(
|input: &'a [u8]| -> IResult<&'a [u8], Option<SmallVec<[Address; 1]>>> {
let (input, _) = tag("(")(input)?;
let (input, envelopes) = fold_many1(
delimited(tag("("), envelope_address, alt((tag(") "), tag(")")))),
delimited(tag("("), envelope_address, tag(")")),
SmallVec::new,
|mut acc, item| {
acc.push(item);
@ -1664,12 +1626,12 @@ pub fn mailbox_token(input: &'_ [u8]) -> IResult<&'_ [u8], std::borrow::Cow<'_,
}
// astring = 1*ASTRING-CHAR / string
pub fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
alt((string_token, astring_char))(input)
}
// string = quoted / literal
pub fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
if let Ok((r, o)) = literal(input) {
return Ok((r, o));
}

View File

@ -41,7 +41,6 @@ use crate::error::{Result, ResultIntoMeliError};
use std::borrow::Cow;
use std::convert::TryInto;
use std::ffi::{CStr, CString};
use std::os::raw::c_int;
pub type UnixTimestamp = u64;
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
@ -75,36 +74,14 @@ extern "C" {
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
}
#[repr(i32)]
#[derive(Copy, Clone)]
#[allow(dead_code)]
enum LocaleCategoryMask {
Time = libc::LC_TIME_MASK,
All = libc::LC_ALL_MASK,
}
#[repr(i32)]
#[derive(Copy, Clone)]
#[allow(dead_code)]
enum LocaleCategory {
Time = libc::LC_TIME,
All = libc::LC_ALL,
}
#[cfg(not(target_os = "netbsd"))]
#[allow(dead_code)]
struct Locale {
mask: LocaleCategoryMask,
category: LocaleCategory,
new_locale: libc::locale_t,
old_locale: libc::locale_t,
}
#[cfg(target_os = "netbsd")]
#[allow(dead_code)]
struct Locale {
mask: LocaleCategoryMask,
category: LocaleCategory,
mask: std::os::raw::c_int,
old_locale: *const std::os::raw::c_char,
}
@ -117,7 +94,7 @@ impl Drop for Locale {
}
#[cfg(target_os = "netbsd")]
unsafe {
let _ = libc::setlocale(self.category as c_int, self.old_locale);
let _ = libc::setlocale(self.mask, self.old_locale);
}
}
}
@ -126,12 +103,11 @@ impl Drop for Locale {
impl Locale {
#[cfg(not(target_os = "netbsd"))]
fn new(
mask: LocaleCategoryMask,
category: LocaleCategory,
mask: std::os::raw::c_int,
locale: *const std::os::raw::c_char,
base: libc::locale_t,
) -> Result<Self> {
let new_locale = unsafe { libc::newlocale(mask as c_int, locale, base) };
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
if new_locale.is_null() {
return Err(nix::Error::last().into());
}
@ -141,32 +117,25 @@ impl Locale {
return Err(nix::Error::last().into());
}
Ok(Locale {
mask,
category,
new_locale,
old_locale,
})
}
#[cfg(target_os = "netbsd")]
fn new(
mask: LocaleCategoryMask,
category: LocaleCategory,
mask: std::os::raw::c_int,
locale: *const std::os::raw::c_char,
_base: libc::locale_t,
) -> Result<Self> {
let old_locale = unsafe { libc::setlocale(category as c_int, std::ptr::null_mut()) };
let old_locale = unsafe { libc::setlocale(mask, std::ptr::null_mut()) };
if old_locale.is_null() {
return Err(nix::Error::last().into());
}
let new_locale = unsafe { libc::setlocale(category as c_int, locale) };
let new_locale = unsafe { libc::setlocale(mask, locale) };
if new_locale.is_null() {
return Err(nix::Error::last().into());
}
Ok(Locale {
mask,
category,
old_locale,
})
Ok(Locale { mask, old_locale })
}
}
@ -197,8 +166,7 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: b
let _with_locale: Option<Result<Locale>> = if posix {
Some(
Locale::new(
LocaleCategoryMask::Time,
LocaleCategory::Time,
libc::LC_TIME,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)
@ -336,8 +304,7 @@ where
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
LocaleCategoryMask::Time,
LocaleCategory::Time,
libc::LC_TIME,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)
@ -398,8 +365,7 @@ where
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
LocaleCategoryMask::Time,
LocaleCategory::Time,
libc::LC_TIME,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)

View File

@ -316,7 +316,6 @@ pub struct _gpgme_sig_notation {
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize], u32>,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_sig_notation() {
assert_eq!(
::std::mem::size_of::<_gpgme_sig_notation>(),
@ -458,7 +457,6 @@ pub struct _gpgme_engine_info {
pub home_dir: *mut ::std::os::raw::c_char,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_engine_info() {
assert_eq!(
::std::mem::size_of::<_gpgme_engine_info>(),
@ -546,7 +544,6 @@ pub struct _gpgme_tofu_info {
pub description: *mut ::std::os::raw::c_char,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_tofu_info() {
assert_eq!(
::std::mem::size_of::<_gpgme_tofu_info>(),
@ -714,7 +711,6 @@ pub struct _gpgme_subkey {
pub keygrip: *mut ::std::os::raw::c_char,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_subkey() {
assert_eq!(
::std::mem::size_of::<_gpgme_subkey>(),
@ -1076,7 +1072,6 @@ pub struct _gpgme_key_sig {
pub _last_notation: gpgme_sig_notation_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_key_sig() {
assert_eq!(
::std::mem::size_of::<_gpgme_key_sig>(),
@ -1346,7 +1341,6 @@ pub struct _gpgme_user_id {
pub last_update: ::std::os::raw::c_ulong,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_user_id() {
assert_eq!(
::std::mem::size_of::<_gpgme_user_id>(),
@ -1562,7 +1556,6 @@ pub struct _gpgme_key {
pub last_update: ::std::os::raw::c_ulong,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_key() {
assert_eq!(
::std::mem::size_of::<_gpgme_key>(),
@ -1915,7 +1908,6 @@ pub struct _gpgme_invalid_key {
pub reason: gpgme_error_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_invalid_key() {
assert_eq!(
::std::mem::size_of::<_gpgme_invalid_key>(),
@ -2111,7 +2103,6 @@ pub struct gpgme_io_event_done_data {
pub op_err: gpgme_error_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout_gpgme_io_event_done_data() {
assert_eq!(
::std::mem::size_of::<gpgme_io_event_done_data>(),
@ -2162,7 +2153,6 @@ pub struct gpgme_io_cbs {
pub event_priv: *mut ::std::os::raw::c_void,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout_gpgme_io_cbs() {
assert_eq!(
::std::mem::size_of::<gpgme_io_cbs>(),
@ -2288,7 +2278,6 @@ pub struct gpgme_data_cbs {
pub release: gpgme_data_release_cb_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout_gpgme_data_cbs() {
assert_eq!(
::std::mem::size_of::<gpgme_data_cbs>(),
@ -2418,7 +2407,6 @@ pub struct _gpgme_op_encrypt_result {
pub invalid_recipients: gpgme_invalid_key_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_encrypt_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_encrypt_result>(),
@ -2526,7 +2514,6 @@ pub struct _gpgme_recipient {
pub status: gpgme_error_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_recipient() {
assert_eq!(
::std::mem::size_of::<_gpgme_recipient>(),
@ -2601,7 +2588,6 @@ pub struct _gpgme_op_decrypt_result {
pub symkey_algo: *mut ::std::os::raw::c_char,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_decrypt_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_decrypt_result>(),
@ -2806,7 +2792,6 @@ pub struct _gpgme_new_signature {
pub sig_class: ::std::os::raw::c_uint,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_new_signature() {
assert_eq!(
::std::mem::size_of::<_gpgme_new_signature>(),
@ -2921,7 +2906,6 @@ pub struct _gpgme_op_sign_result {
pub signatures: gpgme_new_signature_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_sign_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_sign_result>(),
@ -3004,7 +2988,6 @@ pub struct _gpgme_signature {
pub key: gpgme_key_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_signature() {
assert_eq!(
::std::mem::size_of::<_gpgme_signature>(),
@ -3248,7 +3231,6 @@ pub struct _gpgme_op_verify_result {
pub __bindgen_padding_0: u32,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_verify_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_verify_result>(),
@ -3349,7 +3331,6 @@ pub struct _gpgme_import_status {
pub status: ::std::os::raw::c_uint,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_import_status() {
assert_eq!(
::std::mem::size_of::<_gpgme_import_status>(),
@ -3424,7 +3405,6 @@ pub struct _gpgme_op_import_result {
pub skipped_v3_keys: ::std::os::raw::c_int,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_import_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_import_result>(),
@ -3683,7 +3663,6 @@ pub struct _gpgme_op_genkey_result {
pub seckey: gpgme_data_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_genkey_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_genkey_result>(),
@ -4045,7 +4024,6 @@ pub struct _gpgme_trust_item {
pub name: *mut ::std::os::raw::c_char,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_trust_item() {
assert_eq!(
::std::mem::size_of::<_gpgme_trust_item>(),
@ -4246,7 +4224,6 @@ pub struct _gpgme_op_vfs_mount_result {
pub mount_dir: *mut ::std::os::raw::c_char,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_vfs_mount_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_vfs_mount_result>(),
@ -4321,7 +4298,6 @@ pub union gpgme_conf_arg__bindgen_ty_1 {
_bindgen_union_align: u64,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout_gpgme_conf_arg__bindgen_ty_1() {
assert_eq!(
::std::mem::size_of::<gpgme_conf_arg__bindgen_ty_1>(),
@ -4383,7 +4359,6 @@ fn bindgen_test_layout_gpgme_conf_arg__bindgen_ty_1() {
);
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout_gpgme_conf_arg() {
assert_eq!(
::std::mem::size_of::<gpgme_conf_arg>(),
@ -4448,7 +4423,6 @@ pub struct gpgme_conf_opt {
pub user_data: *mut ::std::os::raw::c_void,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout_gpgme_conf_opt() {
assert_eq!(
::std::mem::size_of::<gpgme_conf_opt>(),
@ -4637,7 +4611,6 @@ pub struct gpgme_conf_comp {
pub options: *mut gpgme_conf_opt,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout_gpgme_conf_comp() {
assert_eq!(
::std::mem::size_of::<gpgme_conf_comp>(),
@ -4745,7 +4718,6 @@ pub struct _gpgme_op_query_swdb_result {
pub reldate: ::std::os::raw::c_ulong,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_query_swdb_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_query_swdb_result>(),
@ -5286,7 +5258,6 @@ pub struct _gpgme_op_assuan_result {
pub err: gpgme_error_t,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout__gpgme_op_assuan_result() {
assert_eq!(
::std::mem::size_of::<_gpgme_op_assuan_result>(),
@ -5367,7 +5338,6 @@ pub struct __va_list_tag {
pub reg_save_area: *mut ::std::os::raw::c_void,
}
#[test]
#[allow(deref_nullptr)]
fn bindgen_test_layout___va_list_tag() {
assert_eq!(
::std::mem::size_of::<__va_list_tag>(),

View File

@ -93,22 +93,6 @@ where
}
}
pub fn map_res<'a, P, F, E, A, B>(parser: P, map_fn: F) -> impl Parser<'a, B>
where
P: Parser<'a, A>,
F: Fn(A) -> std::result::Result<B, E>,
{
move |input| {
parser.parse(input).and_then(|(next_input, result)| {
if let Ok(res) = map_fn(result) {
Ok((next_input, res))
} else {
Err(next_input)
}
})
}
}
pub fn match_literal<'a>(expected: &'static str) -> impl Parser<'a, ()> {
move |input: &'a str| match input.get(0..expected.len()) {
Some(next) if next == expected => Ok((&input[expected.len()..], ())),
@ -194,24 +178,6 @@ pub fn quoted_string<'a>() -> impl Parser<'a, String> {
)
}
pub fn quoted_slice<'a>() -> impl Parser<'a, &'a str> {
move |input: &'a str| {
if input.is_empty() || !input.starts_with('"') {
return Err(input);
}
let mut i = 1;
while i < input.len() {
if input[i..].starts_with('\"') && !input[i - 1..].starts_with('\\') {
return Ok((&input[i + 1..], &input[1..i]));
}
i += 1;
}
Err(input)
}
}
pub struct BoxedParser<'a, Output> {
parser: Box<dyn Parser<'a, Output> + 'a>,
}
@ -251,8 +217,6 @@ where
right(space0(), left(parser, space0()))
}
pub use whitespace_wrap as ws_eat;
pub fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
where
P1: Parser<'a, R1>,
@ -326,69 +290,6 @@ pub fn space0<'a>() -> impl Parser<'a, Vec<char>> {
zero_or_more(whitespace_char())
}
pub fn is_a<'a>(slice: &'static [u8]) -> impl Parser<'a, &'a str> {
move |input: &'a str| {
let mut i = 0;
for byte in input.as_bytes().iter() {
if !slice.contains(byte) {
break;
}
i += 1;
}
if i == 0 {
return Err("");
}
let (b, a) = input.split_at(i);
Ok((a, b))
}
}
/// Try alternative parsers in order until one succeeds.
///
/// ```rust
/// # use melib::parsec::{Parser, quoted_slice, match_literal, alt, delimited, prefix};
///
/// let parser = |input| {
/// alt([
/// delimited(
/// match_literal("{"),
/// quoted_slice(),
/// match_literal("}"),
/// ),
/// delimited(
/// match_literal("["),
/// quoted_slice(),
/// match_literal("]"),
/// ),
/// ]).parse(input)
/// };
///
/// let input1: &str = "{\"quoted\"}";
/// let input2: &str = "[\"quoted\"]";
/// assert_eq!(
/// Ok(("", "quoted")),
/// parser.parse(input1)
/// );
///
/// assert_eq!(
/// Ok(("", "quoted")),
/// parser.parse(input2)
/// );
/// ```
pub fn alt<'a, P, A, const N: usize>(parsers: [P; N]) -> impl Parser<'a, A>
where
P: Parser<'a, A>,
{
move |input| {
for parser in parsers.iter() {
if let Ok(res) = parser.parse(input) {
return Ok(res);
}
}
Err(input)
}
}
pub fn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B>
where
P: Parser<'a, A>,
@ -444,198 +345,3 @@ where
Ok((&input[offset..], input))
}
}
pub fn separated_list0<'a, P, A, S, Sep>(
parser: P,
separator: S,
terminated: bool,
) -> impl Parser<'a, Vec<A>>
where
P: Parser<'a, A>,
S: Parser<'a, Sep>,
{
move |mut input| {
let mut result = Vec::new();
let mut prev_sep_result = Ok(());
let mut last_item_input = input;
while let Ok((next_input, next_item)) = parser.parse(input) {
prev_sep_result?;
input = next_input;
last_item_input = next_input;
result.push(next_item);
match separator.parse(input) {
Ok((next_input, _)) => {
input = next_input;
}
Err(err) => {
prev_sep_result = Err(err);
}
}
}
if !terminated {
input = last_item_input;
}
Ok((input, result))
}
}
/// Take `count` bytes
pub fn take<'a>(count: usize) -> impl Parser<'a, &'a str> {
move |i: &'a str| {
if i.len() < count || !i.is_char_boundary(count) {
Err("")
} else {
let (b, a) = i.split_at(count);
Ok((a, b))
}
}
}
/// Take a literal
///
///```rust
/// # use std::str::FromStr;
/// # use melib::parsec::{Parser, delimited, match_literal, map_res, is_a, take_literal};
/// let lit: &str = "{31}\r\nThere is no script by that name\r\n";
/// assert_eq!(
/// take_literal(delimited(
/// match_literal("{"),
/// map_res(is_a(b"0123456789"), |s| usize::from_str(s)),
/// match_literal("}\r\n"),
/// ))
/// .parse(lit),
/// Ok((
/// "\r\n",
/// "There is no script by that name",
/// ))
/// );
///```
pub fn take_literal<'a, P>(parser: P) -> impl Parser<'a, &'a str>
where
P: Parser<'a, usize>,
{
move |input: &'a str| {
let (rest, length) = parser.parse(input)?;
take(length).parse(rest)
}
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
#[test]
fn test_parsec() {
#[derive(Debug, PartialEq)]
enum JsonValue {
JsonString(String),
JsonNumber(f64),
JsonBool(bool),
JsonNull,
JsonObject(HashMap<String, JsonValue>),
JsonArray(Vec<JsonValue>),
}
fn parse_value<'a>() -> impl Parser<'a, JsonValue> {
move |input| {
either(
either(
either(
either(
either(
map(parse_bool(), |b| JsonValue::JsonBool(b)),
map(parse_null(), |()| JsonValue::JsonNull),
),
map(parse_array(), |vec| JsonValue::JsonArray(vec)),
),
map(parse_object(), |obj| JsonValue::JsonObject(obj)),
),
map(parse_number(), |n| JsonValue::JsonNumber(n)),
),
map(quoted_string(), |s| JsonValue::JsonString(s)),
)
.parse(input)
}
}
fn parse_number<'a>() -> impl Parser<'a, f64> {
move |input| {
either(
map(match_literal("TRUE"), |()| 1.0),
map(match_literal("FALSe"), |()| 1.0),
)
.parse(input)
}
}
fn parse_bool<'a>() -> impl Parser<'a, bool> {
move |input| {
ws_eat(either(
map(match_literal("true"), |()| true),
map(match_literal("false"), |()| false),
))
.parse(input)
}
}
fn parse_null<'a>() -> impl Parser<'a, ()> {
move |input| ws_eat(match_literal("null")).parse(input)
}
fn parse_array<'a>() -> impl Parser<'a, Vec<JsonValue>> {
move |input| {
delimited(
ws_eat(match_literal("[")),
separated_list0(parse_value(), ws_eat(match_literal(",")), false),
ws_eat(match_literal("]")),
)
.parse(input)
}
}
fn parse_object<'a>() -> impl Parser<'a, HashMap<String, JsonValue>> {
move |input| {
map(
delimited(
ws_eat(match_literal("{")),
separated_list0(
pair(
suffix(quoted_string(), ws_eat(match_literal(":"))),
parse_value(),
),
ws_eat(match_literal(",")),
false,
),
ws_eat(match_literal("}")),
),
|vec: Vec<(String, JsonValue)>| vec.into_iter().collect(),
)
.parse(input)
}
}
assert_eq!(
Ok(("", JsonValue::JsonString("a".to_string()))),
parse_value().parse(r#""a""#)
);
assert_eq!(
Ok(("", JsonValue::JsonBool(true))),
parse_value().parse(r#"true"#)
);
assert_eq!(
Ok(("", JsonValue::JsonObject(HashMap::default()))),
parse_value().parse(r#"{}"#)
);
println!("{:?}", parse_value().parse(r#"{"a":true}"#));
println!("{:?}", parse_value().parse(r#"{"a":true,"b":false}"#));
println!("{:?}", parse_value().parse(r#"{ "a" : true,"b": false }"#));
println!("{:?}", parse_value().parse(r#"{ "a" : true,"b": false,}"#));
println!("{:?}", parse_value().parse(r#"{"a":false,"b":false,}"#));
// Line:0 Col:18 Error parsing object
// { "a":1, "b" : 2, }
// ^Unexpected ','
}
}

View File

@ -840,6 +840,19 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
}
)
},
{ tags: ["do"],
desc: "perform a shortcut",
tokens: &[One(Literal("do"))],
parser:(
fn do_shortcut(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("do")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
let (input, shortcut) = map_res(not_line_ending, std::str::from_utf8)(input.trim())?;
let (input, _) = eof(input.trim())?;
Ok((input, DoShortcut(shortcut.to_string())))
}
)
},
{ tags: ["quit"],
desc: "quit meli",
tokens: &[One(Literal("quit"))],
@ -961,6 +974,7 @@ pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
account_action,
print_setting,
toggle_mouse,
do_shortcut,
reload_config,
quit,
))(input)

View File

@ -124,33 +124,22 @@ pub enum Action {
PrintSetting(String),
ReloadConfiguration,
ToggleMouse,
DoShortcut(String),
Quit,
}
impl Action {
pub fn needs_confirmation(&self) -> bool {
match self {
Action::Listing(ListingAction::Delete) => true,
Action::Listing(_) => false,
Action::ViewMailbox(_) => false,
Action::Sort(_, _) => false,
Action::SubSort(_, _) => false,
Action::Tab(_) => false,
Action::MailingListAction(_) => true,
Action::View(_) => false,
Action::SetEnv(_, _) => false,
Action::PrintEnv(_) => false,
Action::Compose(_) => false,
Action::Mailbox(_, _) => true,
Action::AccountAction(_, _) => false,
Action::PrintSetting(_) => false,
Action::ToggleMouse => false,
Action::Quit => true,
Action::ReloadConfiguration => false,
}
matches!(
self,
Action::Listing(ListingAction::Delete)
| Action::MailingListAction(_)
| Action::Mailbox(_, _)
| Action::Quit
)
}
}
type AccountName = String;
type MailboxPath = String;
type NewMailboxPath = String;
pub type AccountName = String;
pub type MailboxPath = String;
pub type NewMailboxPath = String;

View File

@ -107,4 +107,6 @@ pub trait Component: Display + Debug + Send + Sync {
fn get_status(&self, _context: &Context) -> String {
String::new()
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()>;
}

View File

@ -295,4 +295,8 @@ impl Component for ContactManager {
self.set_dirty(true);
false
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -638,18 +638,8 @@ impl Component for ContactList {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["create_contact"]) =>
{
let mut manager = ContactManager::new(context);
manager.set_parent_id(self.id);
manager.account_pos = self.account_pos;
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
let _ret = self.perform("create_contact", context);
debug_assert!(_ret.is_ok());
return true;
}
@ -657,121 +647,38 @@ impl Component for ContactList {
if shortcut!(key == shortcuts[Self::DESCRIPTION]["edit_contact"])
&& self.length > 0 =>
{
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
let card = book[&self.id_positions[self.cursor_pos]].clone();
let mut manager = ContactManager::new(context);
manager.set_parent_id(self.id);
manager.card = card;
manager.account_pos = self.account_pos;
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
let _ret = self.perform("edit_contact", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["mail_contact"])
&& self.length > 0 =>
{
let account = &context.accounts[self.account_pos];
let account_hash = account.hash();
let book = &account.address_book;
let card = &book[&self.id_positions[self.cursor_pos]];
let mut draft: Draft = Draft::default();
*draft.headers_mut().get_mut("To").unwrap() =
format!("{} <{}>", &card.name(), &card.email());
let mut composer = Composer::with_account(account_hash, context);
composer.set_draft(draft);
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(composer))))));
let _ret = self.perform("mail_contact", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["next_account"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
if self.accounts.is_empty() {
return true;
}
if self.account_pos + amount < self.accounts.len() {
self.account_pos += amount;
self.set_dirty(true);
self.initialized = false;
self.cursor_pos = 0;
self.new_cursor_pos = 0;
self.length = 0;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context),
)));
}
let _ret = self.perform("next_account", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["prev_account"]) =>
{
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
if self.accounts.is_empty() {
return true;
}
if self.account_pos >= amount {
self.account_pos -= amount;
self.set_dirty(true);
self.cursor_pos = 0;
self.new_cursor_pos = 0;
self.length = 0;
self.initialized = false;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context),
)));
}
let _ret = self.perform("prev_account", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Self::DESCRIPTION]["toggle_menu_visibility"]) =>
{
self.menu_visibility = !self.menu_visibility;
self.set_dirty(true);
let _ret = self.perform("toggle_menu_visibility", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
if !self.cmd_buf.is_empty() =>
@ -957,4 +864,142 @@ impl Component for ContactList {
context.accounts[self.account_pos].address_book.len()
)
}
fn perform(&mut self, mut action: &str, context: &mut Context) -> Result<()> {
if let Some(stripped) = action.strip_prefix("contact_list.") {
action = stripped;
}
match action {
"scroll_up" => Ok(()),
"scroll_down" => Ok(()),
"create_contact" => {
if self.view.is_none() {
let mut manager = ContactManager::new(context);
manager.set_parent_id(self.id);
manager.account_pos = self.account_pos;
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
Ok(())
}
"edit_contact" => {
if self.length > 0 {
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
let card = book[&self.id_positions[self.cursor_pos]].clone();
let mut manager = ContactManager::new(context);
manager.set_parent_id(self.id);
manager.card = card;
manager.account_pos = self.account_pos;
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
Ok(())
}
"mail_contact" => {
if self.length > 0 {
let account = &context.accounts[self.account_pos];
let account_hash = account.hash();
let book = &account.address_book;
let card = &book[&self.id_positions[self.cursor_pos]];
let mut draft: Draft = Draft::default();
*draft.headers_mut().get_mut("To").unwrap() =
format!("{} <{}>", &card.name(), &card.email());
let mut composer = Composer::with_account(account_hash, context);
composer.set_draft(draft);
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(composer))))));
}
Ok(())
}
"next_account" => {
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return Ok(());
};
if self.accounts.is_empty() {
return Ok(());
}
if self.account_pos + amount < self.accounts.len() {
self.account_pos += amount;
self.set_dirty(true);
self.initialized = false;
self.cursor_pos = 0;
self.new_cursor_pos = 0;
self.length = 0;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context),
)));
}
Ok(())
}
"prev_account" => {
let amount = if self.cmd_buf.is_empty() {
1
} else if let Ok(amount) = self.cmd_buf.parse::<usize>() {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
amount
} else {
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return Ok(());
};
if self.accounts.is_empty() {
return Ok(());
}
if self.account_pos >= amount {
self.account_pos -= amount;
self.set_dirty(true);
self.cursor_pos = 0;
self.new_cursor_pos = 0;
self.length = 0;
self.initialized = false;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context),
)));
}
Ok(())
}
"toggle_menu_visibility" => {
self.menu_visibility = !self.menu_visibility;
self.set_dirty(true);
Ok(())
}
other => Err(format!("`{}` is not a valid contact list shortcut.", other).into()),
}
}
}

View File

@ -2102,6 +2102,10 @@ impl Component for Composer {
self.set_dirty(true);
false
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
pub fn send_draft(

View File

@ -309,4 +309,8 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
fn set_id(&mut self, new_id: ComponentId) {
self.inner.id = new_id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -264,6 +264,10 @@ impl Component for KeySelection {
KeySelection::Loaded { ref mut widget, .. } => widget.set_id(new_id),
}
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
#[derive(Debug, Clone)]

View File

@ -1786,6 +1786,10 @@ impl Component for Listing {
MailboxStatus::Failed(_) | MailboxStatus::None => account[&mailbox_hash].status(),
}
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
self.component.perform(action, context)
}
}
impl Listing {

View File

@ -2125,4 +2125,11 @@ impl Component for CompactListing {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
if self.unfocused() {
return self.view.perform(action, context);
}
Ok(())
}
}

View File

@ -1585,4 +1585,11 @@ impl Component for ConversationsListing {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
if self.unfocused() {
return self.view.perform(action, context);
}
Err("No actions available.".into())
}
}

View File

@ -229,4 +229,8 @@ impl Component for OfflineListing {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -1490,4 +1490,11 @@ impl Component for PlainListing {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
if self.unfocused() {
return self.view.perform(action, context);
}
Ok(())
}
}

View File

@ -1519,4 +1519,13 @@ impl Component for ThreadListing {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
if self.unfocused() {
if let Some(p) = self.view.as_mut() {
return p.perform(action, context);
};
}
Ok(())
}
}

View File

@ -466,4 +466,8 @@ impl Component for AccountStatus {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -1928,60 +1928,29 @@ impl Component for MailView {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply"]) =>
{
self.perform_action(PendingReplyAction::Reply, context);
let _ret = self.perform("reply", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply_to_all"]) =>
{
self.perform_action(PendingReplyAction::ReplyToAll, context);
let _ret = self.perform("reply_to_all", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply_to_author"]) =>
{
self.perform_action(PendingReplyAction::ReplyToAuthor, context);
let _ret = self.perform("reply_to_author", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["forward"]) =>
{
match mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.composing
.forward_as_attachment
) {
f if f.is_ask() => {
let id = self.id;
context.replies.push_back(UIEvent::GlobalUIDialog(Box::new(
UIConfirmationDialog::new(
"How do you want the email to be forwarded?",
vec![
(true, "inline".to_string()),
(false, "as attachment".to_string()),
],
true,
Some(Box::new(move |_: ComponentId, result: bool| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(if result {
PendingReplyAction::ForwardInline
} else {
PendingReplyAction::ForwardAttachment
}),
))
})),
context,
),
)));
}
f if f.is_true() => {
self.perform_action(PendingReplyAction::ForwardAttachment, context);
}
_ => {
self.perform_action(PendingReplyAction::ForwardInline, context);
}
}
let _ret = self.perform("forward", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::FinishedUIDialog(id, ref result) if id == self.id() => {
@ -1993,76 +1962,13 @@ impl Component for MailView {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["edit"]) =>
{
let account_hash = self.coordinates.0;
let env_hash = self.coordinates.2;
let (sender, mut receiver) = crate::jobs::oneshot::channel();
let operation = context.accounts[&account_hash].operation(env_hash);
let bytes_job = async move {
let _ = sender.send(operation?.as_bytes()?.await);
Ok(())
};
let handle = if context.accounts[&account_hash]
.backend_capabilities
.is_async
{
context.accounts[&account_hash]
.job_executor
.spawn_specialized(bytes_job)
} else {
context.accounts[&account_hash]
.job_executor
.spawn_blocking(bytes_job)
};
context.accounts[&account_hash].insert_job(
handle.job_id,
crate::conf::accounts::JobRequest::Generic {
name: "fetch envelope".into(),
handle,
on_finish: Some(CallbackFn(Box::new(move |context: &mut Context| {
match receiver.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */
}
Ok(Some(result)) => {
match result.and_then(|bytes| {
Composer::edit(account_hash, env_hash, &bytes, context)
}) {
Ok(composer) => {
context.replies.push_back(UIEvent::Action(Tab(New(Some(
Box::new(composer),
)))));
}
Err(err) => {
let err_string = format!(
"Failed to open envelope {}: {}",
context.accounts[&account_hash]
.collection
.envelopes
.read()
.unwrap()
.get(&env_hash)
.map(|env| env.message_id_display())
.unwrap_or_else(|| "Not found".into()),
err
);
log(&err_string, ERROR);
context.replies.push_back(UIEvent::Notification(
Some("Failed to open e-mail".to_string()),
err_string,
Some(NotificationType::Error(err.kind)),
));
}
}
}
}
}))),
logging_level: melib::LoggingLevel::DEBUG,
},
);
let _ret = self.perform("edit", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Action(View(ViewAction::AddAddressesToContacts)) => {
self.start_contact_selector(context);
let _ret = self.perform("add_addresses_to_contacts", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
@ -2071,7 +1977,8 @@ impl Component for MailView {
key == shortcuts[MailView::DESCRIPTION]["add_addresses_to_contacts"]
) =>
{
self.start_contact_selector(context);
let _ret = self.perform("add_addresses_to_contacts", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
@ -2105,12 +2012,8 @@ impl Component for MailView {
|| self.mode == ViewMode::Source(Source::Raw))
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["view_raw_source"]) =>
{
self.mode = match self.mode {
ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw),
_ => ViewMode::Source(Source::Decoded),
};
self.set_dirty(true);
self.initialised = false;
let _ret = self.perform("view_raw_source", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
@ -2124,9 +2027,8 @@ impl Component for MailView {
key == shortcuts[MailView::DESCRIPTION]["return_to_normal_view"]
) =>
{
self.mode = ViewMode::Normal;
self.set_dirty(true);
self.initialised = false;
let _ret = self.perform("return_to_normal_view", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
@ -2134,33 +2036,8 @@ impl Component for MailView {
&& !self.cmd_buf.is_empty()
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["open_mailcap"]) =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded { .. } => {
if let Some(attachment) = self.open_attachment(lidx, context) {
if let Ok(()) =
crate::mailcap::MailcapEntry::execute(attachment, context)
{
self.set_dirty(true);
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"no mailcap entry found for {}",
attachment.content_type()
)),
));
}
}
}
MailViewState::Init { .. } => {
self.init_futures(context);
}
}
let _ret = self.perform("open_mailcap", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
@ -2168,117 +2045,8 @@ impl Component for MailView {
&& !self.cmd_buf.is_empty()
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview) =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded { .. } => {
if let Some(attachment) = self.open_attachment(lidx, context) {
match attachment.content_type() {
ContentType::MessageRfc822 => {
match Mail::new(attachment.body().to_vec(), Some(Flag::SEEN)) {
Ok(wrapper) => {
context.replies.push_back(UIEvent::Action(Tab(New(
Some(Box::new(EnvelopeView::new(
wrapper,
None,
None,
self.coordinates.0,
))),
))));
}
Err(e) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("{}", e)),
));
}
}
}
ContentType::Text { .. }
| ContentType::PGPSignature
| ContentType::CMSSignature => {
self.mode = ViewMode::Attachment(lidx);
self.initialised = false;
self.dirty = true;
}
ContentType::Multipart { .. } => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(
"Multipart attachments are not supported yet."
.to_string(),
),
));
}
ContentType::Other { .. } => {
let attachment_type = attachment.mime_type();
let filename = attachment.filename();
if let Ok(command) = query_default_app(&attachment_type) {
let p = create_temp_file(
&attachment.decode(Default::default()),
filename.as_deref(),
None,
true,
);
let exec_cmd = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new("sh")
.args(&["-c", &exec_cmd])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => {
context.temp_files.push(p);
context.children.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
)),
));
}
}
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(if let Some(filename) = filename.as_ref() {
format!(
"Couldn't find a default application for file {} (type {})",
filename,
attachment_type
)
} else {
format!(
"Couldn't find a default application for type {}",
attachment_type
)
}),
));
}
}
ContentType::OctetStream { ref name } => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to open {}. application/octet-stream isn't supported yet",
name.as_ref().map(|n| n.as_str()).unwrap_or("file")
)),
));
}
}
}
}
MailViewState::Init { .. } => {
self.init_futures(context);
}
}
let _ret = self.perform("open_attachment", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
@ -2287,8 +2055,8 @@ impl Component for MailView {
key == shortcuts[MailView::DESCRIPTION]["toggle_expand_headers"]
) =>
{
self.expand_headers = !self.expand_headers;
self.set_dirty(true);
let _ret = self.perform("toggle_expand_headers", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
@ -2296,90 +2064,16 @@ impl Component for MailView {
&& self.mode == ViewMode::Url
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["go_to_url"]) =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Init { .. } => {
self.init_futures(context);
}
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded {
body: _,
bytes: _,
display: _,
env: _,
ref body_text,
ref links,
} => {
let (_kind, url) = {
if let Some(l) = links
.get(lidx)
.and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?)))
{
l
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Link `{}` not found.",
lidx
)),
));
return true;
}
};
let url_launcher = mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.pager
.url_launcher
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(
#[cfg(target_os = "macos")]
{
"open"
},
#[cfg(not(target_os = "macos"))]
{
"xdg-open"
},
);
match Command::new(url_launcher)
.arg(url)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => {
context.children.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to launch {:?}", url_launcher)),
err.to_string(),
Some(NotificationType::Error(melib::ErrorKind::External)),
));
}
}
}
}
let _ret = self.perform("go_to_url", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key)
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Url)
&& shortcut!(key == shortcuts[MailView::DESCRIPTION]["toggle_url_mode"]) =>
{
match self.mode {
ViewMode::Normal => self.mode = ViewMode::Url,
ViewMode::Url => self.mode = ViewMode::Normal,
_ => {}
}
self.initialised = false;
self.dirty = true;
let _ret = self.perform("toggle_url_mode", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::EnvelopeRename(old_hash, new_hash) if self.coordinates.2 == old_hash => {
@ -2778,6 +2472,414 @@ impl Component for MailView {
.push_back(UIEvent::Action(Tab(Kill(self.id))));
}
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
match action {
"reply" => {
self.perform_action(PendingReplyAction::Reply, context);
}
"reply_to_all" => {
self.perform_action(PendingReplyAction::ReplyToAll, context);
}
"reply_to_author" => {
self.perform_action(PendingReplyAction::ReplyToAuthor, context);
}
"forward" => {
match mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.composing
.forward_as_attachment
) {
f if f.is_ask() => {
let id = self.id;
context.replies.push_back(UIEvent::GlobalUIDialog(Box::new(
UIConfirmationDialog::new(
"How do you want the email to be forwarded?",
vec![
(true, "inline".to_string()),
(false, "as attachment".to_string()),
],
true,
Some(Box::new(move |_: ComponentId, result: bool| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(if result {
PendingReplyAction::ForwardInline
} else {
PendingReplyAction::ForwardAttachment
}),
))
})),
context,
),
)));
}
f if f.is_true() => {
self.perform_action(PendingReplyAction::ForwardAttachment, context);
}
_ => {
self.perform_action(PendingReplyAction::ForwardInline, context);
}
}
}
"edit" => {
let account_hash = self.coordinates.0;
let env_hash = self.coordinates.2;
let (sender, mut receiver) = crate::jobs::oneshot::channel();
let operation = context.accounts[&account_hash].operation(env_hash);
let bytes_job = async move {
let _ = sender.send(operation?.as_bytes()?.await);
Ok(())
};
let handle = if context.accounts[&account_hash]
.backend_capabilities
.is_async
{
context.accounts[&account_hash]
.job_executor
.spawn_specialized(bytes_job)
} else {
context.accounts[&account_hash]
.job_executor
.spawn_blocking(bytes_job)
};
context.accounts[&account_hash].insert_job(
handle.job_id,
crate::conf::accounts::JobRequest::Generic {
name: "fetch envelope".into(),
handle,
on_finish: Some(CallbackFn(Box::new(move |context: &mut Context| {
match receiver.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */
}
Ok(Some(result)) => {
match result.and_then(|bytes| {
Composer::edit(account_hash, env_hash, &bytes, context)
}) {
Ok(composer) => {
context.replies.push_back(UIEvent::Action(Tab(New(Some(
Box::new(composer),
)))));
}
Err(err) => {
let err_string = format!(
"Failed to open envelope {}: {}",
context.accounts[&account_hash]
.collection
.envelopes
.read()
.unwrap()
.get(&env_hash)
.map(|env| env.message_id_display())
.unwrap_or_else(|| "Not found".into()),
err
);
log(&err_string, ERROR);
context.replies.push_back(UIEvent::Notification(
Some("Failed to open e-mail".to_string()),
err_string,
Some(NotificationType::Error(err.kind)),
));
}
}
}
}
}))),
logging_level: melib::LoggingLevel::DEBUG,
},
);
}
"add_addresses_to_contacts" => {
if !self.mode.is_contact_selector() {
self.start_contact_selector(context);
}
}
"view_raw_source" => {
if matches!(
self.mode,
ViewMode::Normal
| ViewMode::Subview
| ViewMode::Source(Source::Decoded)
| ViewMode::Source(Source::Raw)
) {
self.mode = match self.mode {
ViewMode::Source(Source::Decoded) => ViewMode::Source(Source::Raw),
_ => ViewMode::Source(Source::Decoded),
};
self.set_dirty(true);
self.initialised = false;
}
}
"return_to_normal_view" => {
if self.mode.is_attachment()
|| matches!(
self.mode,
ViewMode::Subview
| ViewMode::Url
| ViewMode::Source(Source::Decoded)
| ViewMode::Source(Source::Raw)
)
{
self.mode = ViewMode::Normal;
self.set_dirty(true);
self.initialised = false;
}
}
"open_mailcap" => {
if (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview)
&& !self.cmd_buf.is_empty()
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded { .. } => {
if let Some(attachment) = self.open_attachment(lidx, context) {
if let Ok(()) =
crate::mailcap::MailcapEntry::execute(attachment, context)
{
self.set_dirty(true);
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"no mailcap entry found for {}",
attachment.content_type()
)),
));
}
}
}
MailViewState::Init { .. } => {
self.init_futures(context);
}
}
}
}
"open_attachment" => {
if self.cmd_buf.is_empty()
&& (self.mode == ViewMode::Normal || self.mode == ViewMode::Subview)
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded { .. } => {
if let Some(attachment) = self.open_attachment(lidx, context) {
match attachment.content_type() {
ContentType::MessageRfc822 => {
match Mail::new(
attachment.body().to_vec(),
Some(Flag::SEEN),
) {
Ok(wrapper) => {
context.replies.push_back(UIEvent::Action(Tab(
New(Some(Box::new(EnvelopeView::new(
wrapper,
None,
None,
self.coordinates.0,
)))),
)));
}
Err(e) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("{}", e)),
));
}
}
}
ContentType::Text { .. }
| ContentType::PGPSignature
| ContentType::CMSSignature => {
self.mode = ViewMode::Attachment(lidx);
self.initialised = false;
self.dirty = true;
}
ContentType::Multipart { .. } => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(
"Multipart attachments are not supported yet."
.to_string(),
),
));
}
ContentType::Other { .. } => {
let attachment_type = attachment.mime_type();
let filename = attachment.filename();
if let Ok(command) = query_default_app(&attachment_type) {
let p = create_temp_file(
&attachment.decode(Default::default()),
filename.as_deref(),
None,
true,
);
let (exec_cmd, argument) = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new(&exec_cmd)
.arg(&argument)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => {
context.temp_files.push(p);
context.children.push(child);
}
Err(err) => {
context.replies.push_back(
UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
),
);
}
}
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(if let Some(filename) = filename.as_ref() {
format!(
"Couldn't find a default application for file {} (type {})",
filename,
attachment_type
)
} else {
format!(
"Couldn't find a default application for type {}",
attachment_type
)
}),
));
}
}
ContentType::OctetStream { ref name } => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to open {}. application/octet-stream isn't supported yet",
name.as_ref().map(|n| n.as_str()).unwrap_or("file")
)),
));
}
}
}
}
MailViewState::Init { .. } => {
self.init_futures(context);
}
}
}
}
"toggle_expand_headers" => {
if self.mode == ViewMode::Normal || self.mode == ViewMode::Url {
self.expand_headers = !self.expand_headers;
self.set_dirty(true);
}
}
"go_to_url" => {
if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url {
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
match self.state {
MailViewState::Init { .. } => {
self.init_futures(context);
}
MailViewState::Error { .. } | MailViewState::LoadingBody { .. } => {}
MailViewState::Loaded {
body: _,
bytes: _,
display: _,
env: _,
ref body_text,
ref links,
} => {
let (_kind, url) = {
if let Some(l) = links
.get(lidx)
.and_then(|l| Some((l.kind, body_text.get(l.start..l.end)?)))
{
l
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Link `{}` not found.",
lidx
)),
));
return Ok(());
}
};
let url_launcher = mailbox_settings!(
context[self.coordinates.0][&self.coordinates.1]
.pager
.url_launcher
)
.as_ref()
.map(|s| s.as_str())
.unwrap_or(
#[cfg(target_os = "macos")]
{
"open"
},
#[cfg(not(target_os = "macos"))]
{
"xdg-open"
},
);
match Command::new(url_launcher)
.arg(url)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => {
context.children.push(child);
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to launch {:?}", url_launcher)),
err.to_string(),
Some(NotificationType::Error(melib::ErrorKind::External)),
));
}
}
}
}
}
}
"toggle_url_mode" => {
if matches!(self.mode, ViewMode::Normal | ViewMode::Url) {
match self.mode {
ViewMode::Normal => self.mode = ViewMode::Url,
ViewMode::Url => self.mode = ViewMode::Normal,
_ => {}
}
self.initialised = false;
self.dirty = true;
}
}
other => {
return Err(format!("Envelope view doesn't have an `{}` action.", other).into())
}
}
Ok(())
}
}
fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
@ -2790,66 +2892,49 @@ fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
Ok(())
}
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> String {
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> (String, String) {
/* Purge unused field codes */
let command = command
.replace("%i", "")
.replace("%c", "")
.replace("%k", "");
if command.contains("%f") {
command.replacen("%f", &path.replace(' ', "\\ "), 1)
} else if command.contains("%F") {
command.replacen("%F", &path.replace(' ', "\\ "), 1)
} else if command.contains("%u") || command.contains("%U") {
let from_pattern = if command.contains("%u") { "%u" } else { "%U" };
if let Some(pos) = command.find("%f").or_else(|| command.find("%F")) {
(command[0..pos].trim().to_string(), path)
} else if let Some(pos) = command.find("%u").or_else(|| command.find("%U")) {
if is_url {
command.replacen(from_pattern, &path, 1)
(command[0..pos].trim().to_string(), path)
} else {
command.replacen(
from_pattern,
&format!("file://{}", path).replace(' ', "\\ "),
1,
(
command[0..pos].trim().to_string(),
format!("file://{}", path),
)
}
} else if is_url {
format!("{} {}", command, path)
} else {
format!("{} {}", command, path.replace(' ', "\\ "))
(command, path)
}
}
/*
#[test]
fn test_desktop_exec() {
assert_eq!(
"ristretto /tmp/file".to_string(),
desktop_exec_to_command("ristretto %F", "/tmp/file".to_string(), false)
);
assert_eq!(
"/usr/lib/firefox-esr/firefox-esr file:///tmp/file".to_string(),
desktop_exec_to_command(
"/usr/lib/firefox-esr/firefox-esr %u",
"/tmp/file".to_string(),
false
)
);
assert_eq!(
"/usr/lib/firefox-esr/firefox-esr www.example.com".to_string(),
desktop_exec_to_command(
"/usr/lib/firefox-esr/firefox-esr %u",
"www.example.com".to_string(),
true
)
);
assert_eq!(
"/usr/bin/vlc --started-from-file www.example.com".to_string(),
desktop_exec_to_command(
"/usr/bin/vlc --started-from-file %U",
"www.example.com".to_string(),
true
)
);
assert_eq!(
"zathura --fork file:///tmp/file".to_string(),
desktop_exec_to_command("zathura --fork %U", "file:///tmp/file".to_string(), true)
);
for cmd in [
"ristretto %F",
"/usr/lib/firefox-esr/firefox-esr %u",
"/usr/bin/vlc --started-from-file %U",
"zathura %U",
]
.iter()
{
println!(
"cmd = {} output = {:?}, is_url = false",
cmd,
desktop_exec_to_command(cmd, "/tmp/file".to_string(), false)
);
println!(
"cmd = {} output = {:?}, is_url = true",
cmd,
desktop_exec_to_command(cmd, "www.example.com".to_string(), true)
);
}
}
*/

View File

@ -403,13 +403,13 @@ impl Component for EnvelopeView {
None,
true,
);
let exec_cmd = super::desktop_exec_to_command(
let (exec_cmd, argument) = super::desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new("sh")
.args(&["-c", &exec_cmd])
match Command::new(&exec_cmd)
.arg(&argument)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -421,8 +421,8 @@ impl Component for EnvelopeView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}
@ -525,11 +525,13 @@ impl Component for EnvelopeView {
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
|| self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|| self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
}
@ -548,4 +550,8 @@ impl Component for EnvelopeView {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -135,6 +135,7 @@ impl Component for HtmlView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
self.pager.draw(grid, area, context);
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if self.pager.process_event(event, context) {
return true;
@ -151,10 +152,10 @@ impl Component for HtmlView {
};
if let Some(command) = command {
let p = create_temp_file(&self.bytes, None, None, true);
let exec_cmd =
let (exec_cmd, argument) =
super::desktop_exec_to_command(&command, p.path.display().to_string(), false);
match Command::new("sh")
.args(&["-c", &exec_cmd])
match Command::new(&exec_cmd)
.arg(&argument)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -166,8 +167,8 @@ impl Component for HtmlView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}
@ -183,12 +184,15 @@ impl Component for HtmlView {
}
false
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
self.pager.get_shortcuts(context)
}
fn is_dirty(&self) -> bool {
self.pager.is_dirty()
}
fn set_dirty(&mut self, value: bool) {
self.pager.set_dirty(value);
}
@ -196,7 +200,12 @@ impl Component for HtmlView {
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
self.pager.perform(action, context)
}
}

View File

@ -1172,4 +1172,11 @@ impl Component for ThreadView {
.replies
.push_back(UIEvent::Action(Tab(Kill(self.id))));
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
if self.show_mailview {
return self.mailview.perform(action, context);
}
Err("No actions available.".into())
}
}

View File

@ -23,6 +23,7 @@
Notification handling components.
*/
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use super::*;
@ -138,6 +139,9 @@ mod dbus {
}
fn set_id(&mut self, _id: ComponentId) {}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
fn escape_str(s: &str) -> String {
@ -177,6 +181,19 @@ impl NotificationCommand {
pub fn new() -> Self {
NotificationCommand {}
}
fn update_xbiff(path: &str) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.append(true) /* writes will append to a file instead of overwriting previous contents */
.create(true) /* a new file will be created if the file does not yet already exist.*/
.open(path)?;
if file.metadata()?.len() > 128 {
file.set_len(0)?;
} else {
std::io::Write::write_all(&mut file, b"z")?;
}
Ok(())
}
}
impl fmt::Display for NotificationCommand {
@ -193,7 +210,7 @@ impl Component for NotificationCommand {
if context.settings.notifications.enable {
if *kind == Some(NotificationType::NewMail) {
if let Some(ref path) = context.settings.notifications.xbiff_file_path {
if let Err(err) = update_xbiff(path) {
if let Err(err) = Self::update_xbiff(path) {
debug!("Could not update xbiff file: {:?}", &err);
melib::log(format!("Could not update xbiff file: {}.", err), ERROR);
}
@ -274,17 +291,291 @@ impl Component for NotificationCommand {
}
fn set_dirty(&mut self, _value: bool) {}
fn set_id(&mut self, _id: ComponentId) {}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
fn update_xbiff(path: &str) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.append(true) /* writes will append to a file instead of overwriting previous contents */
.create(true) /* a new file will be created if the file does not yet already exist.*/
.open(path)?;
if file.metadata()?.len() > 128 {
file.set_len(0)?;
} else {
std::io::Write::write_all(&mut file, b"z")?;
}
Ok(())
#[derive(Debug)]
struct NotificationLog {
title: Option<String>,
body: String,
kind: Option<NotificationType>,
}
/// Notification history
#[derive(Debug)]
pub struct NotificationHistory {
history: Arc<Mutex<IndexMap<std::time::Instant, NotificationLog>>>,
last_update: Arc<Mutex<std::time::Instant>>,
id: ComponentId,
}
/// Notification history view
#[derive(Debug)]
pub struct NotificationHistoryView {
theme_default: ThemeAttribute,
history: Arc<Mutex<IndexMap<std::time::Instant, NotificationLog>>>,
last_update: Arc<Mutex<std::time::Instant>>,
my_last_update: std::time::Instant,
cursor_pos: usize,
dirty: bool,
id: ComponentId,
}
impl Default for NotificationHistory {
fn default() -> Self {
Self::new()
}
}
impl NotificationHistory {
pub fn new() -> Self {
NotificationHistory {
history: Arc::new(Mutex::new(IndexMap::default())),
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
id: ComponentId::new_v4(),
}
}
fn new_view(&self, context: &Context) -> NotificationHistoryView {
NotificationHistoryView {
theme_default: crate::conf::value(context, "theme_default"),
history: self.history.clone(),
last_update: self.last_update.clone(),
my_last_update: std::time::Instant::now(),
cursor_pos: 0,
dirty: true,
id: ComponentId::new_v4(),
}
}
}
impl fmt::Display for NotificationHistory {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "")
}
}
impl fmt::Display for NotificationHistoryView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "notifications")
}
}
impl Component for NotificationHistory {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {}
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
if let UIEvent::Notification(ref title, ref body, ref kind) = event {
self.history.lock().unwrap().insert(
std::time::Instant::now(),
NotificationLog {
title: title.clone(),
body: body.to_string(),
kind: *kind,
},
);
*self.last_update.lock().unwrap() = std::time::Instant::now();
}
false
}
fn id(&self) -> ComponentId {
self.id
}
fn is_dirty(&self) -> bool {
false
}
fn set_dirty(&mut self, _value: bool) {}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
match action {
"clear_history" => {
self.history.lock().unwrap().clear();
*self.last_update.lock().unwrap() = std::time::Instant::now();
Ok(())
}
"open_notification_log" => {
context
.replies
.push_back(UIEvent::Action(Tab(New(Some(Box::new(
self.new_view(context),
))))));
Ok(())
}
_ => Err("No actions available.".into()),
}
}
}
impl Component for NotificationHistoryView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() {
return;
}
self.set_dirty(false);
self.my_last_update = std::time::Instant::now();
clear_area(grid, area, self.theme_default);
context.dirty_areas.push_back(area);
/* reserve top row for column headers */
let upper_left = pos_inc(upper_left!(area), (0, 1));
let bottom_right = bottom_right!(area);
if get_y(bottom_right) < get_y(upper_left) {
return;
}
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
let page_no = (self.cursor_pos).wrapping_div(rows);
let top_idx = page_no * rows;
for (i, (instant, log)) in self
.history
.lock()
.unwrap()
.iter()
.rev()
.skip(top_idx)
.enumerate()
{
let (x, _) = write_string_to_grid(
&i.to_string(),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (0, i)), bottom_right),
None,
);
let (x, _) = write_string_to_grid(
&format!("{:#?}", instant),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
let (x, _) = write_string_to_grid(
&format!("{:?}", log.kind),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
let (x, _) = write_string_to_grid(
log.title.as_deref().unwrap_or_default(),
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
write_string_to_grid(
&log.body,
grid,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (x + 2, i)), bottom_right),
None,
);
}
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match event {
UIEvent::ConfigReload { old_settings: _ } => {
self.theme_default = crate::conf::value(context, "theme_default");
self.set_dirty(true);
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_up"]) => {
let _ret = self.perform("scroll_up", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_down"]) => {
let _ret = self.perform("scroll_down", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_right"]) => {
let _ret = self.perform("scroll_right", context);
debug_assert!(_ret.is_ok());
return true;
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["scroll_left"]) => {
let _ret = self.perform("scroll_left", context);
debug_assert!(_ret.is_ok());
return true;
}
_ => {}
}
false
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map: ShortcutMaps = Default::default();
let config_map = context.settings.shortcuts.general.key_values();
map.insert("general", config_map);
map
}
fn id(&self) -> ComponentId {
self.id
}
fn is_dirty(&self) -> bool {
*self.last_update.lock().unwrap() > self.my_last_update || self.dirty
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
if value {
self.my_last_update = *self.last_update.lock().unwrap();
}
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn kill(&mut self, uuid: Uuid, context: &mut Context) {
debug_assert!(uuid == self.id);
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
}
fn perform(&mut self, action: &str, _context: &mut Context) -> Result<()> {
match action {
"scroll_up" | "scroll_down" | "scroll_right" | "scroll_left" => {
if action == "scroll_up" {
self.cursor_pos = self.cursor_pos.saturating_sub(1);
} else if action == "scroll_down" {
self.cursor_pos = std::cmp::min(
self.cursor_pos + 1,
self.history.lock().unwrap().len().saturating_sub(1),
);
}
self.set_dirty(true);
Ok(())
}
_ => Err("No actions available.".into()),
}
}
}

View File

@ -436,6 +436,10 @@ impl Component for SVGScreenshotFilter {
ComponentId::nil()
}
fn set_id(&mut self, _id: ComponentId) {}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
const CSS_STYLE: &str = r#"#t{font-family:'DejaVu Sans Mono',monospace;font-style:normal;font-size:14px;} text {dominant-baseline: text-before-edge; white-space: pre;} .f{fill:#e5e5e5;} .b{fill:#000;} .c0 {fill:#000;} .c1 {fill:#cd0000;} .c2 {fill:#00cd00;} .c3 {fill:#cdcd00;} .c4 {fill:#00e;} .c5 {fill:#cd00cd;} .c6 {fill:#00cdcd;} .c7 {fill:#e5e5e5;} .c8 {fill:#7f7f7f;} .c9 {fill:#f00;} .c10 {fill:#0f0;} .c11 {fill:#ff0;} .c12 {fill:#5c5cff;} .c13 {fill:#f0f;} .c14 {fill:#0ff;} .c15 {fill:#fff;}"#;

View File

@ -796,6 +796,10 @@ impl Component for StatusBar {
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
self.container.can_quit_cleanly(context)
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
self.container.perform(action, context)
}
}
#[derive(Debug)]
@ -1545,6 +1549,14 @@ impl Component for Tabbed {
}
true
}
fn perform(&mut self, action: &str, context: &mut Context) -> Result<()> {
if !self.children.is_empty() {
self.children[self.cursor_pos].perform(action, context)
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -1627,6 +1639,10 @@ impl Component for RawBuffer {
fn id(&self) -> ComponentId {
ComponentId::nil()
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
impl RawBuffer {

View File

@ -419,9 +419,14 @@ impl<T: 'static + PartialEq + Debug + Clone + Sync + Send> Component for UIDialo
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
impl Component for UIConfirmationDialog {
@ -748,6 +753,10 @@ impl Component for UIConfirmationDialog {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
impl<T: PartialEq + Debug + Clone + Sync + Send, F: 'static + Sync + Send> Selector<T, F> {

View File

@ -110,9 +110,14 @@ impl Component for HSplit {
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
/// A vertically split in half container.
@ -250,7 +255,12 @@ impl Component for VSplit {
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -839,4 +839,8 @@ impl Component for Pager {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -366,15 +366,22 @@ impl Component for Field {
self.set_dirty(true);
true
}
fn is_dirty(&self) -> bool {
false
}
fn set_dirty(&mut self, _value: bool) {}
fn id(&self) -> ComponentId {
ComponentId::nil()
}
fn set_id(&mut self, _id: ComponentId) {}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
impl fmt::Display for Field {
@ -714,9 +721,11 @@ impl<T: 'static + std::fmt::Debug + Copy + Default + Send + Sync> Component for
}
false
}
fn is_dirty(&self) -> bool {
self.dirty || self.buttons.is_dirty()
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
self.buttons.set_dirty(value);
@ -725,9 +734,14 @@ impl<T: 'static + std::fmt::Debug + Copy + Default + Send + Sync> Component for
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
#[derive(Debug, Default)]
@ -855,9 +869,11 @@ where
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
}
@ -865,9 +881,14 @@ where
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
@ -988,12 +1009,15 @@ impl Component for AutoComplete {
}
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool {
false
}
fn is_dirty(&self) -> bool {
self.dirty
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
}
@ -1001,9 +1025,14 @@ impl Component for AutoComplete {
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
impl AutoComplete {
@ -1454,4 +1483,8 @@ impl Component for ProgressSpinner {
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}

View File

@ -86,8 +86,7 @@ macro_rules! shortcut_key_values {
pub struct $name:ident { $($fname:ident |> $fdesc:literal |> $default:expr),* }) => {
$(#[$outer])*
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
#[serde(rename = $cname)]
#[serde(default, deny_unknown_fields, rename = $cname)]
pub struct $name {
$(pub $fname : Key),*
}
@ -100,12 +99,28 @@ macro_rules! shortcut_key_values {
_ => unreachable!()
}
}
/// Returns a hashmap of all shortcuts and their values
pub fn key_values(&self) -> IndexMap<&'static str, Key> {
[
$((stringify!($fname),(self.$fname).clone()),)*
].iter().cloned().collect()
}
/// Returns a slice of all shortcuts.
pub fn key_slice(&self) -> &'static [&'static str] {
use std::sync::Once;
static mut VAL: Vec<&'static str> = vec![];
static INIT: Once = Once::new();
unsafe {
INIT.call_once(|| {
$(VAL.push(stringify!($fname));)*
});
VAL.as_ref()
}
}
}
impl Default for $name {

View File

@ -214,8 +214,7 @@ impl MailcapEntry {
std::borrow::Cow::from("less")
};
let mut pager = Command::new("sh")
.args(["-c", pager_cmd.as_ref()])
let mut pager = Command::new(pager_cmd.as_ref())
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.spawn()?;

View File

@ -233,13 +233,12 @@ fn run_app(opt: Opt) -> Result<()> {
}
use std::process::{Command, Stdio};
let mut handle = Command::new("sh")
.arg("-c")
.arg(std::env::var("PAGER").unwrap_or_else(|_| "more".to_string()))
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
let mut handle =
Command::new(std::env::var("PAGER").unwrap_or_else(|_| "more".to_string()))
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
handle.stdin.take().unwrap().write_all(v.as_bytes())?;
handle.wait()?;
@ -345,6 +344,9 @@ fn run_app(opt: Opt) -> Result<()> {
state.register_component(Box::new(
components::notifications::NotificationCommand::new(),
));
state.register_component(Box::new(
components::notifications::NotificationHistory::new(),
));
}
let enter_command_mode: Key = state
.context

View File

@ -24,15 +24,17 @@ extern crate melib;
use melib::*;
use std::collections::VecDeque;
extern crate xdg_utils;
#[macro_use]
extern crate serde_derive;
extern crate linkify;
extern crate uuid;
extern crate serde_json;
extern crate smallvec;
extern crate termion;
use melib::backends::imap::managesieve::ManageSieveConnection;
use melib::backends::imap::managesieve::new_managesieve_connection;
use melib::Result;
#[macro_use]
@ -62,7 +64,7 @@ pub mod sqlite3;
pub mod jobs;
pub mod mailcap;
//pub mod plugins;
pub mod plugins;
use futures::executor::block_on;
@ -82,7 +84,10 @@ fn main() -> Result<()> {
std::process::exit(1);
}
let (config_path, account_name) = (std::mem::take(&mut args[0]), std::mem::take(&mut args[1]));
let (config_path, account_name) = (
std::mem::replace(&mut args[0], String::new()),
std::mem::replace(&mut args[1], String::new()),
);
std::env::set_var("MELI_CONFIG", config_path);
let settings = conf::Settings::new()?;
if !settings.accounts.contains_key(&account_name) {
@ -97,47 +102,12 @@ fn main() -> Result<()> {
);
std::process::exit(1);
}
let account = &settings.accounts[&account_name].account;
let mut conn = ManageSieveConnection::new(
0,
account_name.clone(),
account,
melib::backends::BackendEventConsumer::new(std::sync::Arc::new(|_, _| {})),
)?;
block_on(conn.inner.connect())?;
let mut conn = new_managesieve_connection(&settings.accounts[&account_name].account)?;
block_on(conn.connect())?;
let mut res = String::with_capacity(8 * 1024);
let mut input = String::new();
const AVAILABLE_COMMANDS: &[&str] = &[
"help",
"logout",
"listscripts",
"checkscript",
"putscript",
"setactive",
"getscript",
"deletescript",
];
const COMMANDS_HELP: &[&str] = &[
"help",
"logout",
"listscripts and whether they are active",
"paste a script to check for validity without uploading it",
"upload a script",
"set a script as active",
"get a script by its name",
"delete a script by its name",
];
println!("managesieve shell: use 'help' for available commands");
enum PrevCmd {
None,
Checkscript,
PutscriptName,
PutscriptString(String),
SetActiveName,
GetScriptName,
}
use PrevCmd::*;
let mut prev_cmd: PrevCmd = None;
println!("managesieve shell: use 'logout'");
loop {
use std::io;
use std::io::Write;
@ -146,85 +116,12 @@ fn main() -> Result<()> {
io::stdout().flush().unwrap();
match io::stdin().read_line(&mut input) {
Ok(_) => {
let input = input.trim();
if input.eq_ignore_ascii_case("logout") {
if input.trim().eq_ignore_ascii_case("logout") {
break;
}
if input.eq_ignore_ascii_case("help") {
println!("available commands: [{}]", AVAILABLE_COMMANDS.join(", "));
continue;
}
if input.len() >= "help ".len()
&& input[0.."help ".len()].eq_ignore_ascii_case("help ")
{
if let Some(i) = AVAILABLE_COMMANDS
.iter()
.position(|cmd| cmd.eq_ignore_ascii_case(&input["help ".len()..]))
{
println!("{}", COMMANDS_HELP[i]);
} else {
println!("invalid command `{}`", &input["help ".len()..]);
}
continue;
}
if input.eq_ignore_ascii_case("listscripts") {
let scripts = block_on(conn.listscripts())?;
println!("Got {} scripts:", scripts.len());
for (script, active) in scripts {
println!(
"{}active: {}",
if active { "" } else { "in" },
String::from_utf8_lossy(&script)
);
}
} else if input.eq_ignore_ascii_case("checkscript") {
prev_cmd = Checkscript;
println!("insert file path of script");
} else if input.eq_ignore_ascii_case("putscript") {
prev_cmd = PutscriptName;
println!("Insert script name");
} else if input.eq_ignore_ascii_case("setactive") {
prev_cmd = SetActiveName;
} else if input.eq_ignore_ascii_case("getscript") {
prev_cmd = GetScriptName;
} else if input.eq_ignore_ascii_case("deletescript") {
println!("unimplemented `{}`", input);
} else {
match prev_cmd {
None => println!("invalid command `{}`", input),
Checkscript => {
let content = std::fs::read_to_string(&input).unwrap();
let result = block_on(conn.checkscript(content.as_bytes()));
println!("Got {:?}", result);
prev_cmd = None;
}
PutscriptName => {
prev_cmd = PutscriptString(input.to_string());
println!("insert file path of script");
}
PutscriptString(name) => {
prev_cmd = None;
let content = std::fs::read_to_string(&input).unwrap();
let result =
block_on(conn.putscript(name.as_bytes(), content.as_bytes()));
println!("Got {:?}", result);
}
SetActiveName => {
prev_cmd = None;
let result = block_on(conn.setactive(input.as_bytes()));
println!("Got {:?}", result);
}
GetScriptName => {
prev_cmd = None;
let result = block_on(conn.getscript(input.as_bytes()));
println!("Got {:?}", result);
}
}
}
//block_on(conn.send_command(input.as_bytes()))?;
//block_on(conn.read_lines(&mut res, String::new()))?;
//println!("out: {}", res.trim());
block_on(conn.send_command(input.as_bytes()))?;
block_on(conn.read_lines(&mut res, String::new()))?;
println!("out: {}", res.trim());
}
Err(error) => println!("error: {}", error),
}

View File

@ -971,6 +971,30 @@ impl State {
)))
.unwrap();
}
DoShortcut(action) => {
let Self {
ref mut components,
ref mut context,
ref mut overlay,
..
} = self;
let mut failure: Option<MeliError> = None;
for c in overlay.iter_mut().chain(components.iter_mut()) {
if let Err(err) = c.perform(action.as_str(), context) {
failure = Some(err);
} else {
failure = None;
break;
}
}
if let Some(err) = failure {
context.replies.push_back(UIEvent::Notification(
None,
err.to_string(),
Some(NotificationType::Error(ErrorKind::None)),
));
}
}
v => {
self.rcv_event(UIEvent::Action(v));
}

View File

@ -389,6 +389,10 @@ impl Component for EmbedContainer {
fn id(&self) -> ComponentId {
self.id
}
fn perform(&mut self, _action: &str, _context: &mut Context) -> Result<()> {
Err("No actions available.".into())
}
}
fn main() -> std::io::Result<()> {