summaryrefslogtreecommitdiff
path: root/melib/src/backends/imap/managesieve.rs
diff options
context:
space:
mode:
Diffstat (limited to 'melib/src/backends/imap/managesieve.rs')
-rw-r--r--melib/src/backends/imap/managesieve.rs491
1 files changed, 402 insertions, 89 deletions
diff --git a/melib/src/backends/imap/managesieve.rs b/melib/src/backends/imap/managesieve.rs
index 56e3edd9..67c036c6 100644
--- a/melib/src/backends/imap/managesieve.rs
+++ b/melib/src/backends/imap/managesieve.rs
@@ -21,16 +21,22 @@
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, error::Error as NomError, error::ErrorKind,
- multi::separated_list1, sequence::separated_pair, IResult,
+ branch::alt, bytes::complete::tag, combinator::map, multi::separated_list1,
+ sequence::separated_pair,
};
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"),
@@ -42,26 +48,225 @@ pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
Ok(ret)
}
-#[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"[..])]
-
- );
+#[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]>,
+ },
+}
+
+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(NomError {
- input,
- code: ErrorKind::Tag,
- }));
+ return Err(nom::Err::Error((input, "empty").into()));
}
let mut i = 1;
@@ -72,91 +277,199 @@ pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
i += 1;
}
- Err(nom::Err::Error(NomError {
- input,
- code: ErrorKind::Tag,
- }))
-}
-
-pub trait ManageSieve {
- fn havespace(&mut self) -> Result<()>;
- fn putscript(&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<()>;
+ Err(nom::Err::Error((input, "no quotes").into()))
}
-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 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),
+ })
+ }
-impl ManageSieve for ImapConnection {
- fn havespace(&mut self) -> Result<()> {
+ pub async 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(())
+ 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 setactive(&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 getscript(&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()),
+ }
}
- fn deletescript(&mut self) -> Result<()> {
- Ok(())
+ 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())
}
- fn renamescript(&mut self) -> Result<()> {
+
+ 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<()> {
Ok(())
}
}