Compare commits

..

2 Commits

Author SHA1 Message Date
Manos Pitsidianakis a05f36341b melib/jmap: add some connection status messages and errors 2022-10-06 16:44:40 +03:00
Manos Pitsidianakis 659a34bf21 accounts: add new IsOnline type and exponential backoff connect retry
Replace unlimited instant retry to connect with a limited exponential
backoff strategy.
2022-10-06 16:33:01 +03:00
32 changed files with 583 additions and 1432 deletions

View File

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

View File

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

View File

@ -303,15 +303,18 @@ On startup, meli should evaluate this command which if successful must only retu
.Ss JMAP only
JMAP specific options
.Bl -tag -width 36n
.It Ic server_url Ar String
.It Ic server_hostname Ar String
example:
.Qq http://mail.example.com
.Qq http://mail.example.com:8080
.Qq https://mail.example.com
.Qq mail.example.com
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
Server password
.It Ic server_port Ar number
.Pq Em optional
The port to connect to
.\" default value
.Pq Em 443
.It Ic danger_accept_invalid_certs Ar boolean
.Pq Em optional
Do not validate TLS certificates.

View File

@ -77,16 +77,6 @@
### Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
#composing.store_sent_mail = false
#
##[accounts."jmap account"]
##root_mailbox = "INBOX"
##format = "jmap"
##server_url="http://localhost:8080"
##server_username="user@hostname.local"
##server_password="changeme"
##listing.index_style = "Conversations"
##identity = "user@hostname.local"
##subscribed_mailboxes = ["*", ]
##composing.send_mail = 'server_submission'
#
#[pager]
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"

View File

@ -105,6 +105,12 @@ impl Default for Backends {
}
}
#[cfg(feature = "jmap_backend")]
pub const JMAP_ERROR_MSG: &str = "";
#[cfg(not(feature = "jmap_backend"))]
pub const JMAP_ERROR_MSG: &str = "This library build lacks JMAP support. JMAP requires an HTTP client dependency and thus is turned off by default when compiling.";
#[cfg(feature = "notmuch_backend")]
pub const NOTMUCH_ERROR_MSG: &str =
"libnotmuch5 was not found in your system. Make sure it is installed and in the library paths. For a custom file path, use `library_file_path` setting in your notmuch account.\n";
@ -219,6 +225,11 @@ impl Backends {
{
eprint!("{}", NOTMUCH_ERROR_DETAILS);
}
} else if key == "jmap" {
#[cfg(not(feature = "jmap_backend"))]
{
eprintln!("{}", JMAP_ERROR_MSG);
}
}
panic!("{} is not a valid mail backend", key);
}
@ -247,6 +258,8 @@ impl Backends {
key,
if cfg!(feature = "notmuch_backend") && key == "notmuch" {
NOTMUCH_ERROR_DETAILS
} else if !cfg!(feature = "jmap_backend") && key == "jmap" {
JMAP_ERROR_MSG
} else {
""
},

View File

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

View File

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

View File

@ -100,9 +100,10 @@ pub struct EnvelopeCache {
#[derive(Debug, Clone)]
pub struct JmapServerConf {
pub server_url: String,
pub server_hostname: String,
pub server_username: String,
pub server_password: String,
pub server_port: u16,
pub danger_accept_invalid_certs: bool,
pub timeout: Option<Duration>,
}
@ -138,9 +139,10 @@ macro_rules! get_conf_val {
impl JmapServerConf {
pub fn new(s: &AccountSettings) -> Result<Self> {
Ok(JmapServerConf {
server_url: get_conf_val!(s["server_url"])?.to_string(),
server_hostname: get_conf_val!(s["server_hostname"])?.to_string(),
server_username: get_conf_val!(s["server_username"])?.to_string(),
server_password: get_conf_val!(s["server_password"])?.to_string(),
server_port: get_conf_val!(s["server_port"], 443)?,
danger_accept_invalid_certs: get_conf_val!(s["danger_accept_invalid_certs"], false)?,
timeout: get_conf_val!(s["timeout"], 16_u64).map(|t| {
if t == 0 {
@ -453,14 +455,7 @@ impl MailBackend for JmapType {
};
let res_text = res.text().await?;
let upload_response: UploadResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let upload_response: UploadResponse = serde_json::from_str(&res_text)?;
let mut req = Request::new(conn.request_no.clone());
let creation_id: Id<EmailObject> = "1".to_string().into();
let mut email_imports = HashMap::default();
@ -484,14 +479,7 @@ impl MailBackend for JmapType {
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text)?;
let m = ImportResponse::try_from(v.method_responses.remove(0)).or_else(|err| {
let ierr: Result<ImportError> =
serde_json::from_str(&res_text).map_err(|err| err.into());
@ -565,14 +553,7 @@ impl MailBackend for JmapType {
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m;
@ -668,14 +649,7 @@ impl MailBackend for JmapType {
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated {
@ -780,14 +754,7 @@ impl MailBackend for JmapType {
*{"methodResponses":[["Email/set",{"notUpdated":null,"notDestroyed":null,"oldState":"86","newState":"87","accountId":"u148940c7","updated":{"M045926eed54b11423918f392":{"id":"M045926eed54b11423918f392"}},"created":null,"destroyed":null,"notCreated":null},"m3"]],"sessionState":"cyrus-0;p-5;vfs-0"}
*/
//debug!("res_text = {}", &res_text);
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated {
@ -937,9 +904,10 @@ impl JmapType {
.unwrap_or_else(|| Ok($default))
};
}
get_conf_val!(s["server_url"])?;
get_conf_val!(s["server_hostname"])?;
get_conf_val!(s["server_username"])?;
get_conf_val!(s["server_password"])?;
get_conf_val!(s["server_port"], 443)?;
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
Ok(())
}

View File

@ -20,6 +20,7 @@
*/
use super::*;
use crate::error::IntoMeliError;
use isahc::config::Configurable;
use std::sync::MutexGuard;
@ -34,6 +35,12 @@ pub struct JmapConnection {
impl JmapConnection {
pub fn new(server_conf: &JmapServerConf, store: Arc<Store>) -> Result<Self> {
(store.event_consumer)(
store.account_hash,
crate::backends::BackendEvent::AccountStateChange {
message: "Creating connection.".into(),
},
);
let client = HttpClient::builder()
.timeout(std::time::Duration::from_secs(10))
.redirect_policy(RedirectPolicy::Limit(10))
@ -53,25 +60,42 @@ impl JmapConnection {
})
}
#[inline(always)]
pub(crate) fn account_state_change(&self, message: impl Into<std::borrow::Cow<'static, str>>) {
let message = message.into();
(self.store.event_consumer)(
self.store.account_hash,
crate::backends::BackendEvent::AccountStateChange { message },
);
}
pub async fn connect(&mut self) -> Result<()> {
if self.store.online_status.lock().await.1.is_ok() {
return Ok(());
}
let mut jmap_session_resource_url = self.server_conf.server_url.to_string();
let mut jmap_session_resource_url = self.server_conf.server_hostname.to_string();
if self.server_conf.server_port != 443 {
jmap_session_resource_url.push(':');
jmap_session_resource_url.push_str(&self.server_conf.server_port.to_string());
}
jmap_session_resource_url.push_str("/.well-known/jmap");
self.account_state_change(format!("Requesting {}", &jmap_session_resource_url));
let mut req = self.client.get_async(&jmap_session_resource_url).await.map_err(|err| {
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server url setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nError connecting to server: {}", &self.server_conf.server_url, &err)).set_source(Some(Arc::new(err)));
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
err
})?;
let mut req = match self.client.get_async(&jmap_session_resource_url).await {
Err(err) => {
let err = err.set_err_summary(format!("Could not connect to JMAP server endpoint for {}. Is your server hostname setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nError connecting to server.", &self.server_conf.server_hostname));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(r) => r,
};
if !req.status().is_success() {
let kind: crate::error::NetworkErrorKind = req.status().into();
let res_text = req.text().await.unwrap_or_default();
let err = MeliError::new(format!(
"Could not connect to JMAP server endpoint for {}. Reply from server: {}",
&self.server_conf.server_url, res_text
&self.server_conf.server_hostname, res_text
))
.set_kind(kind.into());
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
@ -82,7 +106,7 @@ impl JmapConnection {
let session: JmapSession = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server url setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err)));
let err = err.set_err_summary(format!("Could not connect to JMAP server endpoint for {}. Is your server hostname setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nReply from server: {}", &self.server_conf.server_hostname, &res_text));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -92,7 +116,7 @@ impl JmapConnection {
.capabilities
.contains_key("urn:ietf:params:jmap:core")
{
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -100,7 +124,7 @@ impl JmapConnection {
.capabilities
.contains_key("urn:ietf:params:jmap:mail")
{
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -192,14 +216,7 @@ impl JmapConnection {
let res_text = res.text().await?;
debug!(&res_text);
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
let changes_response =
ChangesResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if changes_response.new_state == current_state {

View File

@ -842,8 +842,7 @@ pub struct EmailQueryChangesResponse {
impl std::convert::TryFrom<&RawValue> for EmailQueryChangesResponse {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<EmailQueryChangesResponse> {
let res: (String, EmailQueryChangesResponse, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug))?;
let res: (String, EmailQueryChangesResponse, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, "Email/queryChanges");
Ok(res.1)
}

View File

@ -184,8 +184,7 @@ pub struct ImportResponse {
impl std::convert::TryFrom<&RawValue> for ImportResponse {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<ImportResponse> {
let res: (String, ImportResponse, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug))?;
let res: (String, ImportResponse, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &ImportCall::NAME);
Ok(res.1)
}

View File

@ -85,6 +85,7 @@ pub struct JsonResponse<'a> {
}
pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash, JmapMailbox>> {
conn.account_state_change("Fetching mailbox list…");
let seq = get_request_no!(conn.request_no);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
@ -102,14 +103,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = GetResponse::<MailboxObject>::try_from(v.method_responses.remove(0))?;
let GetResponse::<MailboxObject> {
@ -187,6 +181,7 @@ pub async fn get_message_list(
conn: &JmapConnection,
mailbox: &JmapMailbox,
) -> Result<Vec<Id<EmailObject>>> {
conn.account_state_change(format!("Fetching email list for {}", mailbox.name()));
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id().clone())
@ -209,14 +204,7 @@ pub async fn get_message_list(
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m;
@ -289,14 +277,7 @@ pub async fn fetch(
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let query_response = QueryResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
store

View File

@ -413,8 +413,7 @@ pub struct GetResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<GetResponse<OBJ>, crate::error::MeliError> {
let res: (String, GetResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, GetResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
Ok(res.1)
}
@ -518,8 +517,7 @@ pub struct QueryResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<QueryResponse<OBJ>, crate::error::MeliError> {
let res: (String, QueryResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, QueryResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
Ok(res.1)
}
@ -653,8 +651,7 @@ pub struct ChangesResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<ChangesResponse<OBJ>, crate::error::MeliError> {
let res: (String, ChangesResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, ChangesResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
Ok(res.1)
}
@ -852,8 +849,7 @@ pub struct SetResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<SetResponse<OBJ>, crate::error::MeliError> {
let res: (String, SetResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, SetResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/set", OBJ::NAME));
Ok(res.1)
}

View File

@ -205,7 +205,7 @@ impl MailBackend for MaildirType {
let unseen = mailbox.unseen.clone();
let total = mailbox.total.clone();
let path: PathBuf = mailbox.fs_path().into();
let root_mailbox = self.path.to_path_buf();
let root_path = self.path.to_path_buf();
let map = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
super::stream::MaildirStream::new(
@ -214,7 +214,7 @@ impl MailBackend for MaildirType {
unseen,
total,
path,
root_mailbox,
root_path,
map,
mailbox_index,
)
@ -231,7 +231,7 @@ impl MailBackend for MaildirType {
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
let path: PathBuf = mailbox.fs_path().into();
let root_mailbox = self.path.to_path_buf();
let root_path = self.path.to_path_buf();
let map = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
@ -266,7 +266,7 @@ impl MailBackend for MaildirType {
.lock()
.unwrap()
.insert(env.hash(), mailbox_hash);
let file_name = file.strip_prefix(&root_mailbox).unwrap().to_path_buf();
let file_name = file.strip_prefix(&root_path).unwrap().to_path_buf();
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
/* place result in cache directory */
let f = fs::File::create(cached)?;
@ -334,12 +334,10 @@ impl MailBackend for MaildirType {
hasher.write(self.name.as_bytes());
hasher.finish()
};
let root_mailbox = self.path.to_path_buf();
watcher
.watch(&root_mailbox, RecursiveMode::Recursive)
.unwrap();
let root_path = self.path.to_path_buf();
watcher.watch(&root_path, RecursiveMode::Recursive).unwrap();
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
debug!("watching {:?}", root_mailbox);
debug!("watching {:?}", root_path);
let hash_indexes = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
let root_mailbox_hash: MailboxHash = self
@ -387,7 +385,7 @@ impl MailBackend for MaildirType {
let mailbox_hash = get_path_hash!(pathbuf);
let file_name = pathbuf
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Ok(env) = add_path_to_index(
@ -431,7 +429,7 @@ impl MailBackend for MaildirType {
&mut hash_indexes_lock.entry(mailbox_hash).or_default();
let file_name = pathbuf
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
/* Linear search in hash_index to find old hash */
@ -593,7 +591,7 @@ impl MailBackend for MaildirType {
);
let file_name = dest
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
drop(hash_indexes_lock);
@ -683,7 +681,7 @@ impl MailBackend for MaildirType {
}
let file_name = dest
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
debug!("filename = {:?}", file_name);
@ -732,7 +730,7 @@ impl MailBackend for MaildirType {
drop(hash_indexes_lock);
let file_name = dest
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Ok(env) = add_path_to_index(
@ -1162,29 +1160,24 @@ impl MaildirType {
}
Ok(children)
}
let root_mailbox = PathBuf::from(settings.root_mailbox()).expand();
if !root_mailbox.exists() {
let root_path = PathBuf::from(settings.root_mailbox()).expand();
if !root_path.exists() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
"Configuration error ({}): root_path `{}` is not a valid directory.",
settings.name(),
settings.root_mailbox.as_str()
)));
} else if !root_mailbox.is_dir() {
} else if !root_path.is_dir() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a directory.",
"Configuration error ({}): root_path `{}` is not a directory.",
settings.name(),
settings.root_mailbox.as_str()
)));
}
if let Ok(f) = MaildirMailbox::new(
root_mailbox.to_str().unwrap().to_string(),
root_mailbox
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string(),
root_path.to_str().unwrap().to_string(),
root_path.file_name().unwrap().to_str().unwrap().to_string(),
None,
Vec::with_capacity(0),
false,
@ -1194,7 +1187,7 @@ impl MaildirType {
}
if mailboxes.is_empty() {
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = None;
@ -1202,7 +1195,7 @@ impl MaildirType {
}
} else {
let root_hash = *mailboxes.keys().next().unwrap();
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = Some(root_hash);
@ -1236,7 +1229,7 @@ impl MaildirType {
mailbox_index: Default::default(),
event_consumer,
collection: Default::default(),
path: root_mailbox,
path: root_path,
}))
}
@ -1310,16 +1303,16 @@ impl MaildirType {
}
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let root_mailbox = PathBuf::from(s.root_mailbox()).expand();
if !root_mailbox.exists() {
let root_path = PathBuf::from(s.root_mailbox()).expand();
if !root_path.exists() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
"Configuration error ({}): root_path `{}` is not a valid directory.",
s.name(),
s.root_mailbox.as_str()
)));
} else if !root_mailbox.is_dir() {
} else if !root_path.is_dir() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a directory.",
"Configuration error ({}): root_path `{}` is not a directory.",
s.name(),
s.root_mailbox.as_str()
)));

View File

@ -46,7 +46,7 @@ impl MaildirStream {
unseen: Arc<Mutex<usize>>,
total: Arc<Mutex<usize>>,
mut path: PathBuf,
root_mailbox: PathBuf,
root_path: PathBuf,
map: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
@ -73,7 +73,7 @@ impl MaildirStream {
mailbox_hash,
unseen.clone(),
total.clone(),
root_mailbox.clone(),
root_path.clone(),
map.clone(),
mailbox_index.clone(),
)) as Pin<Box<dyn Future<Output = _> + Send + 'static>>
@ -91,7 +91,7 @@ impl MaildirStream {
mailbox_hash: MailboxHash,
unseen: Arc<Mutex<usize>>,
total: Arc<Mutex<usize>>,
root_mailbox: PathBuf,
root_path: PathBuf,
map: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Vec<Envelope>> {
@ -102,7 +102,7 @@ impl MaildirStream {
/* Check if we have a cache file with this email's
* filename */
let file_name = PathBuf::from(&file)
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Some(cached) = cache_dir.find_cache_file(&file_name) {

View File

@ -475,7 +475,7 @@ impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRef<'_, K, V> {
type Target = V;
fn deref(&self) -> &V {
self.guard.get(&self.hash).expect("Hash was not found")
self.guard.get(&self.hash).unwrap()
}
}
@ -486,7 +486,7 @@ pub struct RwRefMut<'g, K: std::cmp::Eq + std::hash::Hash, V> {
impl<K: std::cmp::Eq + std::hash::Hash, V> DerefMut for RwRefMut<'_, K, V> {
fn deref_mut(&mut self) -> &mut V {
self.guard.get_mut(&self.hash).expect("Hash was not found")
self.guard.get_mut(&self.hash).unwrap()
}
}
@ -494,6 +494,6 @@ impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRefMut<'_, K, V> {
type Target = V;
fn deref(&self) -> &V {
self.guard.get(&self.hash).expect("Hash was not found")
self.guard.get(&self.hash).unwrap()
}
}

View File

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

View File

@ -24,7 +24,7 @@ use std::fs::File;
use std::io::prelude::*;
use std::time::SystemTime;
fn random_u64() -> u64 {
pub fn random_u64() -> u64 {
let mut f = File::open("/dev/urandom").unwrap();
let mut buffer = [0; 8];

View File

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

View File

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

View File

@ -179,9 +179,7 @@ macro_rules! make {
/// use melib::thread::SubjectPrefix;
///
/// let mut subject = "Re: RE: Res: Re: Res: Subject";
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES, None), &"Subject");
/// let mut subject = "Re: RE: Res: Re: Res: Subject";
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES, Some(1)), &"RE: Res: Re: Res: Subject");
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES), &"Subject");
/// ```
pub trait SubjectPrefix {
const USUAL_PREFIXES: &'static [&'static str] = &[
@ -281,7 +279,7 @@ pub trait SubjectPrefix {
];
fn is_a_reply(&self) -> bool;
fn strip_prefixes(&mut self) -> &mut Self;
fn strip_prefixes_from_list(&mut self, list: &[&str], times: Option<u8>) -> &mut Self;
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self;
}
impl SubjectPrefix for &[u8] {
@ -337,10 +335,10 @@ impl SubjectPrefix for &[u8] {
self
}
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self {
let result = {
let mut slice = self.trim();
'outer: loop {
loop {
let len = slice.len();
for prefix in list.iter() {
if slice
@ -349,14 +347,10 @@ impl SubjectPrefix for &[u8] {
.unwrap_or(false)
{
slice = &slice[prefix.len()..];
slice = slice.trim();
times = times.map(|u| u.saturating_sub(1));
if times == Some(0) {
break 'outer;
}
}
slice = slice.trim();
}
if slice.len() == len || times == Some(0) {
if slice.len() == len {
break;
}
}
@ -414,10 +408,10 @@ impl SubjectPrefix for &str {
self
}
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self {
let result = {
let mut slice = self.trim();
'outer: loop {
loop {
let len = slice.len();
for prefix in list.iter() {
if slice
@ -426,14 +420,10 @@ impl SubjectPrefix for &str {
.unwrap_or(false)
{
slice = &slice[prefix.len()..];
slice = slice.trim();
times = times.map(|u| u.saturating_sub(1));
if times == Some(0) {
break 'outer;
}
}
slice = slice.trim();
}
if slice.len() == len || times == Some(0) {
if slice.len() == len {
break;
}
}

View File

@ -228,21 +228,21 @@ impl Composer {
.as_ref()
.map(|v| v.iter().map(String::as_str).collect::<Vec<&str>>())
.unwrap_or_default();
let subject_stripped = subject.as_ref().strip_prefixes_from_list(
if prefix_list.is_empty() {
let subject = subject
.as_ref()
.strip_prefixes_from_list(if prefix_list.is_empty() {
<&str>::USUAL_PREFIXES
} else {
&prefix_list
},
Some(1),
) == &subject.as_ref();
})
.to_string();
let prefix =
account_settings!(context[ret.account_hash].composing.reply_prefix).as_str();
if subject_stripped {
if !subject.starts_with(prefix) {
format!("{prefix} {subject}", prefix = prefix, subject = subject)
} else {
subject.to_string()
subject
}
};
ret.draft.set_header("Subject", subject);
@ -2390,61 +2390,3 @@ fn attribution_string(
);
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
}
#[test]
fn test_compose_reply_subject_prefix() {
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
Cc:
Subject: RE: your e-mail
Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
Content-Type: text/plain
hello world.
"#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let mut context = Context::new_mock();
let account_hash = context.accounts[0].hash();
let mailbox_hash = 0;
let envelope_hash = envelope.hash();
context.accounts[0]
.collection
.insert(envelope, mailbox_hash);
let composer = Composer::reply_to(
(account_hash, mailbox_hash, envelope_hash),
String::new(),
&mut context,
false,
);
assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail");
assert_eq!(
&composer.draft.headers()["To"],
r#"some name <some@example.com>"#
);
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
Cc:
Subject: your e-mail
Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
Content-Type: text/plain
hello world.
"#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let envelope_hash = envelope.hash();
context.accounts[0]
.collection
.insert(envelope, mailbox_hash);
let composer = Composer::reply_to(
(account_hash, mailbox_hash, envelope_hash),
String::new(),
&mut context,
false,
);
assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail");
assert_eq!(
&composer.draft.headers()["To"],
r#"some name <some@example.com>"#
);
}

View File

@ -2223,13 +2223,13 @@ impl Component for MailView {
None,
true,
);
let exec_cmd = desktop_exec_to_command(
let (exec_cmd, argument) = 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()
@ -2241,8 +2241,8 @@ impl Component for MailView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}
@ -2790,66 +2790,49 @@ fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
Ok(())
}
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> String {
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> (String, String) {
/* Purge unused field codes */
let command = command
.replace("%i", "")
.replace("%c", "")
.replace("%k", "");
if command.contains("%f") {
command.replacen("%f", &path.replace(' ', "\\ "), 1)
} else if command.contains("%F") {
command.replacen("%F", &path.replace(' ', "\\ "), 1)
} else if command.contains("%u") || command.contains("%U") {
let from_pattern = if command.contains("%u") { "%u" } else { "%U" };
if let Some(pos) = command.find("%f").or_else(|| command.find("%F")) {
(command[0..pos].trim().to_string(), path)
} else if let Some(pos) = command.find("%u").or_else(|| command.find("%U")) {
if is_url {
command.replacen(from_pattern, &path, 1)
(command[0..pos].trim().to_string(), path)
} else {
command.replacen(
from_pattern,
&format!("file://{}", path).replace(' ', "\\ "),
1,
(
command[0..pos].trim().to_string(),
format!("file://{}", path),
)
}
} else if is_url {
format!("{} {}", command, path)
} else {
format!("{} {}", command, path.replace(' ', "\\ "))
(command, path)
}
}
/*
#[test]
fn test_desktop_exec() {
assert_eq!(
"ristretto /tmp/file".to_string(),
desktop_exec_to_command("ristretto %F", "/tmp/file".to_string(), false)
);
assert_eq!(
"/usr/lib/firefox-esr/firefox-esr file:///tmp/file".to_string(),
desktop_exec_to_command(
"/usr/lib/firefox-esr/firefox-esr %u",
"/tmp/file".to_string(),
false
)
);
assert_eq!(
"/usr/lib/firefox-esr/firefox-esr www.example.com".to_string(),
desktop_exec_to_command(
"/usr/lib/firefox-esr/firefox-esr %u",
"www.example.com".to_string(),
true
)
);
assert_eq!(
"/usr/bin/vlc --started-from-file www.example.com".to_string(),
desktop_exec_to_command(
"/usr/bin/vlc --started-from-file %U",
"www.example.com".to_string(),
true
)
);
assert_eq!(
"zathura --fork file:///tmp/file".to_string(),
desktop_exec_to_command("zathura --fork %U", "file:///tmp/file".to_string(), true)
);
for cmd in [
"ristretto %F",
"/usr/lib/firefox-esr/firefox-esr %u",
"/usr/bin/vlc --started-from-file %U",
"zathura %U",
]
.iter()
{
println!(
"cmd = {} output = {:?}, is_url = false",
cmd,
desktop_exec_to_command(cmd, "/tmp/file".to_string(), false)
);
println!(
"cmd = {} output = {:?}, is_url = true",
cmd,
desktop_exec_to_command(cmd, "www.example.com".to_string(), true)
);
}
}
*/

View File

@ -403,13 +403,13 @@ impl Component for EnvelopeView {
None,
true,
);
let exec_cmd = super::desktop_exec_to_command(
let (exec_cmd, argument) = super::desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new("sh")
.args(&["-c", &exec_cmd])
match Command::new(&exec_cmd)
.arg(&argument)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -421,8 +421,8 @@ impl Component for EnvelopeView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}

View File

@ -151,10 +151,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 +166,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
)),
));
}

View File

@ -155,24 +155,24 @@ impl FileMailboxConf {
use crate::conf::deserializers::extra_settings;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileAccount {
pub root_mailbox: String,
pub format: String,
pub identity: String,
root_mailbox: String,
format: String,
identity: String,
#[serde(default)]
pub extra_identities: Vec<String>,
extra_identities: Vec<String>,
#[serde(default = "none")]
pub display_name: Option<String>,
display_name: Option<String>,
#[serde(default = "false_val")]
pub read_only: bool,
read_only: bool,
#[serde(default)]
pub subscribed_mailboxes: Vec<String>,
subscribed_mailboxes: Vec<String>,
#[serde(default)]
pub mailboxes: IndexMap<String, FileMailboxConf>,
mailboxes: IndexMap<String, FileMailboxConf>,
#[serde(default)]
pub search_backend: SearchBackend,
search_backend: SearchBackend,
#[serde(default)]
pub order: (SortField, SortOrder),
order: (SortField, SortOrder),
#[serde(default = "false_val")]
pub manual_refresh: bool,
#[serde(default = "none")]
@ -345,22 +345,18 @@ impl FileSettings {
if path_string.is_empty() {
return Err(MeliError::new("No configuration found."));
}
#[cfg(not(test))]
let ask = Ask {
message: format!(
"No configuration found. Would you like to generate one in {}?",
path_string
),
};
#[cfg(not(test))]
if ask.run() {
create_config_file(&config_path)?;
return Err(MeliError::new(
"Edit the sample configuration and relaunch meli.",
));
}
#[cfg(test)]
return Ok(FileSettings::default());
return Err(MeliError::new("No configuration file found."));
}
@ -924,9 +920,9 @@ mod pp {
#[serde(deny_unknown_fields)]
pub struct LogSettings {
#[serde(default)]
pub log_file: Option<PathBuf>,
log_file: Option<PathBuf>,
#[serde(default)]
pub maximum_level: melib::LoggingLevel,
maximum_level: melib::LoggingLevel,
}
pub use dotaddressable::*;
@ -1265,7 +1261,7 @@ send_mail = '/bin/false'
.write_all("[composing]\nsend_mail = '/bin/false'\n".as_bytes())
.unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert_eq!(err.summary.as_ref(), "Configuration error (account-name): root_mailbox `/path/to/root/mailbox` is not a valid directory.");
assert_eq!(err.summary.as_ref(), "Configuration error (account-name): root_path `/path/to/root/mailbox` is not a valid directory.");
/* Test unrecognised configuration entries error */

View File

@ -25,6 +25,7 @@
use super::{AccountConf, FileMailboxConf};
use crate::jobs::{JobExecutor, JobId, JoinHandle};
use crate::RateLimit;
use indexmap::IndexMap;
use melib::backends::*;
use melib::email::*;
@ -130,11 +131,47 @@ impl MailboxEntry {
}
}
#[derive(Debug)]
pub enum IsOnline {
Unitialized {
last_activity: std::time::Instant,
rate_limit: RateLimit,
},
Yes {
last_activity: std::time::Instant,
},
No {
last_activity: std::time::Instant,
err: MeliError,
},
NoWillRetry {
last_activity: std::time::Instant,
err: MeliError,
rate_limit: RateLimit,
},
}
impl IsOnline {
#[inline(always)]
pub fn as_err(&self) -> Option<&MeliError> {
if let IsOnline::No { ref err, .. } | IsOnline::NoWillRetry { ref err, .. } = self {
Some(err)
} else {
None
}
}
#[inline(always)]
pub fn is_ok(&self) -> bool {
matches!(&self, IsOnline::Yes { .. })
}
}
#[derive(Debug)]
pub struct Account {
name: String,
hash: AccountHash,
pub is_online: Result<()>,
pub is_online: IsOnline,
pub(crate) mailbox_entries: IndexMap<MailboxHash, MailboxEntry>,
pub(crate) mailboxes_order: Vec<MailboxHash>,
tree: Vec<MailboxNode>,
@ -338,6 +375,10 @@ impl core::fmt::Display for JobRequest {
}
impl JobRequest {
pub fn is_mailboxes(&self) -> bool {
matches!(self, JobRequest::Mailboxes { .. })
}
pub fn is_watch(&self) -> bool {
matches!(self, JobRequest::Watch { .. })
}
@ -518,9 +559,14 @@ impl Account {
hash,
name,
is_online: if !backend.capabilities().is_remote {
Ok(())
IsOnline::Yes {
last_activity: std::time::Instant::now(),
}
} else {
Err(MeliError::new("Attempting connection."))
IsOnline::Unitialized {
last_activity: std::time::Instant::now(),
rate_limit: RateLimit::new(1, 1, job_executor.clone()).exponential_backoff(5),
}
},
mailbox_entries: Default::default(),
mailboxes_order: Default::default(),
@ -1555,36 +1601,25 @@ impl Account {
/* Call only in Context::is_online, since only Context can launch the watcher threads if an
* account goes from offline to online. */
pub fn is_online(&mut self) -> Result<()> {
if !self.backend_capabilities.is_remote && !self.backend_capabilities.is_async {
return Ok(());
}
if self.is_online.is_err()
&& self
.is_online
.as_ref()
.unwrap_err()
.kind
.is_authentication()
{
return self.is_online.clone();
}
if self.is_online.is_ok() {
return Ok(());
}
if !self.active_jobs.values().any(JobRequest::is_online) {
let online_job = self.backend.read().unwrap().is_online();
if let Ok(online_job) = online_job {
let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(online_job)
} else {
self.job_executor.spawn_blocking(online_job)
};
self.insert_job(handle.job_id, JobRequest::IsOnline { handle });
pub fn is_online(&mut self) -> &IsOnline {
match &self.is_online {
_ if !self.backend_capabilities.is_remote && !self.backend_capabilities.is_async => {}
IsOnline::Unitialized { .. } | IsOnline::No { .. } | IsOnline::Yes { .. } => {}
IsOnline::NoWillRetry { .. }
if self.active_jobs.values().any(JobRequest::is_online) => {}
IsOnline::NoWillRetry { .. } => {
let online_job = self.backend.read().unwrap().is_online();
if let Ok(online_job) = online_job {
let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(online_job)
} else {
self.job_executor.spawn_blocking(online_job)
};
self.insert_job(handle.job_id, JobRequest::IsOnline { handle });
}
}
}
self.is_online.clone()
&self.is_online
}
pub fn search(
@ -1632,6 +1667,135 @@ impl Account {
}
}
pub fn update_status(&mut self, result: Result<()>) {
if let Err(err) = result {
if err.kind.is_authentication() {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::Notification(
Some(format!("{}: authentication error", &self.name)),
err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)),
)))
.expect("Could not send event on main channel");
self.is_online = IsOnline::No {
last_activity: std::time::Instant::now(),
err,
};
return;
}
if let IsOnline::NoWillRetry {
ref mut last_activity,
err: ref mut err_ptr,
ref mut rate_limit,
} = self.is_online
{
if rate_limit.active {
*last_activity = std::time::Instant::now();
*err_ptr = err;
} else {
self.is_online = IsOnline::No {
last_activity: std::time::Instant::now(),
err,
};
}
} else {
self.is_online = IsOnline::NoWillRetry {
last_activity: std::time::Instant::now(),
err,
rate_limit: RateLimit::new(1, 1, self.job_executor.clone())
.exponential_backoff(5),
};
}
} else {
if self.mailbox_entries.is_empty() {
let backend = self.backend.read().unwrap();
if let Ok(mailboxes_job) = backend.mailboxes() {
if let Ok(online_job) = backend.is_online() {
let handle = if backend.capabilities().is_async {
self.job_executor
.spawn_specialized(online_job.then(|_| mailboxes_job))
} else {
self.job_executor
.spawn_blocking(online_job.then(|_| mailboxes_job))
};
let job_id = handle.job_id;
self.active_jobs
.insert(job_id, JobRequest::Mailboxes { handle });
self.active_job_instants
.insert(std::time::Instant::now(), job_id);
self.sender
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::NewJob(job_id),
)))
.unwrap();
}
}
self.is_online = IsOnline::Unitialized {
last_activity: std::time::Instant::now(),
rate_limit: RateLimit::new(1, 1, self.job_executor.clone())
.exponential_backoff(5),
};
} else {
if let IsOnline::Yes {
ref mut last_activity,
} = self.is_online
{
*last_activity = std::time::Instant::now();
} else {
self.is_online = IsOnline::Yes {
last_activity: std::time::Instant::now(),
};
}
}
}
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash, None,
)))
.unwrap();
}
pub fn retry_mailboxes(&mut self) {
match self.is_online {
IsOnline::NoWillRetry {
ref rate_limit,
ref err,
last_activity,
} if !rate_limit.active => {
self.is_online = IsOnline::No {
err: err.clone(),
last_activity,
};
return;
}
IsOnline::NoWillRetry { .. } | IsOnline::Unitialized { .. } => {}
_ => return,
}
let backend = self.backend.read().unwrap();
if let Ok(mailboxes_job) = backend.mailboxes() {
if let Ok(online_job) = backend.is_online() {
let handle = if backend.capabilities().is_async {
self.job_executor
.spawn_specialized(online_job.then(|_| mailboxes_job))
} else {
self.job_executor
.spawn_blocking(online_job.then(|_| mailboxes_job))
};
let job_id = handle.job_id;
self.active_jobs
.insert(job_id, JobRequest::Mailboxes { handle });
self.active_job_instants
.insert(std::time::Instant::now(), job_id);
self.sender
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::NewJob(job_id),
)))
.unwrap();
}
}
}
pub fn process_event(&mut self, job_id: &JobId) -> bool {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
@ -1644,26 +1808,8 @@ impl Account {
JobRequest::Mailboxes { ref mut handle } => {
if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
if err.kind.is_authentication() {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::Notification(
Some(format!("{}: authentication error", &self.name)),
err.to_string(),
Some(crate::types::NotificationType::Error(err.kind)),
)))
.expect("Could not send event on main channel");
self.is_online = Err(err);
return true;
}
let mailboxes_job = self.backend.read().unwrap().mailboxes();
if let Ok(mailboxes_job) = mailboxes_job {
let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(mailboxes_job)
} else {
self.job_executor.spawn_blocking(mailboxes_job)
};
self.insert_job(handle.job_id, JobRequest::Mailboxes { handle });
};
self.update_status(Err(err));
return true;
} else {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
@ -1770,20 +1916,13 @@ impl Account {
)))
.unwrap();
if is_online.is_ok() {
if self.is_online.is_err()
&& !self
.is_online
.as_ref()
.unwrap_err()
.kind
.is_authentication()
{
if self.is_online.as_err().is_none() {
self.watch();
}
self.is_online = Ok(());
self.update_status(Ok(()));
return true;
}
self.is_online = is_online;
self.update_status(is_online);
}
let online_job = self.backend.read().unwrap().is_online();
if let Ok(online_job) = online_job {
@ -1800,31 +1939,10 @@ impl Account {
Err(_) => { /* canceled */ }
Ok(None) => {}
Ok(Some(Ok(()))) => {
if self.is_online.is_err()
&& !self
.is_online
.as_ref()
.unwrap_err()
.kind
.is_authentication()
{
self.update_status(Ok(()));
if self.is_online.as_err().is_none() {
self.watch();
}
if !(self.is_online.is_err()
&& self
.is_online
.as_ref()
.unwrap_err()
.kind
.is_authentication())
{
self.is_online = Ok(());
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash, None,
)))
.unwrap();
}
}
Ok(Some(Err(err))) => {
if !err.kind.is_authentication() {
@ -1838,7 +1956,7 @@ impl Account {
self.insert_job(handle.job_id, JobRequest::IsOnline { handle });
};
}
self.is_online = Err(err);
self.update_status(Err(err));
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash, None,

View File

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

View File

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

View File

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

View File

@ -134,6 +134,11 @@ impl Context {
} = self;
let was_online = accounts[account_pos].is_online.is_ok();
let ret = accounts[account_pos].is_online();
let ret = if let Some(err) = ret.as_err() {
Err(err.clone())
} else {
Ok(())
};
if ret.is_ok() && !was_online {
debug!("inserting mailbox hashes:");
for mailbox_node in accounts[account_pos].list_mailboxes() {
@ -163,75 +168,6 @@ impl Context {
let idx = self.accounts.get_index_of(&account_hash).unwrap();
self.is_online_idx(idx)
}
#[cfg(test)]
pub fn new_mock() -> Self {
let (sender, receiver) =
crossbeam::channel::bounded(32 * ::std::mem::size_of::<ThreadEvent>());
let job_executor = Arc::new(JobExecutor::new(sender.clone()));
let input_thread = unbounded();
let input_thread_pipe = nix::unistd::pipe()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
.unwrap();
let backends = Backends::new();
let settings = Box::new(Settings::new().unwrap());
let accounts = vec![{
let name = "test".to_string();
let mut account_conf = AccountConf::default();
account_conf.conf.format = "maildir".to_string();
account_conf.account.format = "maildir".to_string();
account_conf.account.root_mailbox = "/tmp/".to_string();
let sender = sender.clone();
let account_hash = {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
hasher.finish()
};
Account::new(
account_hash,
name,
account_conf,
&backends,
job_executor.clone(),
sender.clone(),
BackendEventConsumer::new(Arc::new(
move |account_hash: AccountHash, ev: BackendEvent| {
sender
.send(ThreadEvent::UIEvent(UIEvent::BackendEvent(
account_hash,
ev,
)))
.unwrap();
},
)),
)
.unwrap()
}];
let accounts = accounts.into_iter().map(|acc| (acc.hash(), acc)).collect();
let working = Arc::new(());
let control = Arc::downgrade(&working);
Context {
accounts,
settings,
dirty_areas: VecDeque::with_capacity(0),
replies: VecDeque::with_capacity(0),
temp_files: Vec::new(),
job_executor,
children: vec![],
input_thread: InputHandler {
pipe: input_thread_pipe,
rx: input_thread.1,
tx: input_thread.0,
control,
state_tx: sender.clone(),
},
sender,
receiver,
}
}
}
/// A State object to manage and own components and components of the UI. `State` is responsible for
@ -1109,6 +1045,22 @@ impl State {
self.redraw();
return;
}
UIEvent::Timer(id) => {
for i in 0..self.context.accounts.len() {
use crate::conf::accounts::IsOnline;
match self.context.accounts[i].is_online {
IsOnline::Unitialized { ref rate_limit, .. }
| IsOnline::NoWillRetry { ref rate_limit, .. }
if rate_limit.id() == id =>
{
self.context.accounts[i].retry_mailboxes();
return;
}
_ => {}
}
}
}
UIEvent::Input(ref key)
if *key
== self

View File

@ -316,6 +316,7 @@ pub struct RateLimit {
pub timer: crate::jobs::Timer,
rate: std::time::Duration,
pub active: bool,
retries: Option<u8>,
}
impl RateLimit {
@ -328,9 +329,20 @@ impl RateLimit {
),
rate: std::time::Duration::from_millis(millis / reqs),
active: false,
retries: None,
}
}
pub fn once(mut self) -> Self {
self.retries = Some(1);
self
}
pub fn exponential_backoff(mut self, n: u8) -> Self {
self.retries = Some(n);
self
}
pub fn reset(&mut self) {
self.last_tick = std::time::Instant::now();
self.active = false;
@ -341,9 +353,22 @@ impl RateLimit {
if self.last_tick + self.rate > now {
self.active = false;
} else {
self.timer.rearm();
self.last_tick = now;
self.active = true;
let jitter = if self.retries.is_some() {
Some(melib::email::compose::random::random_u64() % 1000)
} else {
None
};
if let Some(v) = self.retries {
self.retries = Some(v.saturating_sub(1));
}
self.active = self.retries.is_none() || self.retries != Some(0);
if self.active {
self.timer.rearm();
self.last_tick = now;
if let Some(jitter) = jitter {
self.last_tick += std::time::Duration::from_millis(jitter);
}
}
}
self.active
}