Compare commits
3 Commits
master
...
feature/pe
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 792fcee954 | |
Manos Pitsidianakis | 4085622a1c | |
Manos Pitsidianakis | 96f9aa8072 |
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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>(),
|
||||
|
|
|
@ -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 ','
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<()>;
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;}"#;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()?;
|
||||
|
|
16
src/main.rs
16
src/main.rs
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
24
src/state.rs
24
src/state.rs
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
Loading…
Reference in New Issue