|
|
|
/*
|
|
|
|
* meli - managesieve
|
|
|
|
*
|
|
|
|
* Copyright 2020 Manos Pitsidianakis
|
|
|
|
*
|
|
|
|
* This file is part of meli.
|
|
|
|
*
|
|
|
|
* meli is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* meli is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
use std::{
|
|
|
|
str::FromStr,
|
|
|
|
sync::{Arc, Mutex},
|
|
|
|
time::SystemTime,
|
|
|
|
};
|
|
|
|
|
|
|
|
use super::{ImapConnection, ImapProtocol, ImapServerConf, UIDStore};
|
|
|
|
use crate::{
|
|
|
|
conf::AccountSettings,
|
|
|
|
email::parser::IResult,
|
|
|
|
error::{Error, Result},
|
|
|
|
get_conf_val,
|
|
|
|
imap::RequiredResponses,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub struct ManageSieveConnection {
|
|
|
|
pub inner: ImapConnection,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
pub enum ManageSieveResponse<'a> {
|
|
|
|
Ok {
|
|
|
|
code: Option<&'a [u8]>,
|
|
|
|
message: Option<&'a [u8]>,
|
|
|
|
},
|
|
|
|
NoBye {
|
|
|
|
code: Option<&'a [u8]>,
|
|
|
|
message: Option<&'a [u8]>,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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(Error::new("Account is uninitialised.")),
|
|
|
|
))),
|
|
|
|
..UIDStore::new(
|
|
|
|
account_hash,
|
|
|
|
account_name.into(),
|
|
|
|
event_consumer,
|
|
|
|
server_conf.timeout,
|
|
|
|
)
|
|
|
|
});
|
|
|
|
Ok(Self {
|
|
|
|
inner: ImapConnection::new_connection(
|
|
|
|
&server_conf,
|
|
|
|
"ManageSieveConnection::new()".into(),
|
|
|
|
uid_store,
|
|
|
|
),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn havespace(&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(String::from_utf8_lossy).unwrap_or_default(),
|
|
|
|
message.map(String::from_utf8_lossy).unwrap_or_default()
|
|
|
|
)
|
|
|
|
.into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn listscripts(&mut self) -> Result<Vec<(Vec<u8>, bool)>> {
|
|
|
|
let mut ret = Vec::new();
|
|
|
|
self.inner.send_command_raw(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)>>())
|
|
|
|
}
|
|
|
|
|
|
|
|
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(String::from_utf8_lossy).unwrap_or_default(),
|
|
|
|
message.map(String::from_utf8_lossy).unwrap_or_default()
|
|
|
|
)
|
|
|
|
.into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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(String::from_utf8_lossy).unwrap_or_default(),
|
|
|
|
message.map(String::from_utf8_lossy).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(String::from_utf8_lossy).unwrap_or_default(),
|
|
|
|
message.map(String::from_utf8_lossy).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(String::from_utf8_lossy).unwrap_or_default(),
|
|
|
|
message.map(String::from_utf8_lossy).unwrap_or_default()
|
|
|
|
)
|
|
|
|
.into()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn renamescript(&mut self) -> Result<()> {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub mod parser {
|
|
|
|
use nom::{
|
|
|
|
branch::alt,
|
|
|
|
bytes::complete::tag,
|
|
|
|
character::complete::crlf,
|
|
|
|
combinator::{iterator, map, opt},
|
|
|
|
multi::separated_list1,
|
|
|
|
sequence::separated_pair,
|
|
|
|
};
|
|
|
|
pub use nom::{
|
|
|
|
bytes::complete::{is_not, tag_no_case},
|
|
|
|
sequence::{delimited, pair, preceded, terminated},
|
|
|
|
};
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
/// 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()));
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut i = 1;
|
|
|
|
while i < input.len() {
|
|
|
|
if input[i] == b'\"' && input[i - 1] != b'\\' {
|
|
|
|
return Ok((&input[i + 1..], &input[1..i]));
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
Err(nom::Err::Error((input, "no quotes").into()))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
|
|
|
|
let (_, ret) = separated_list1(
|
|
|
|
tag(b"\r\n"),
|
|
|
|
alt((
|
|
|
|
separated_pair(quoted_raw, tag(b" "), quoted_raw),
|
|
|
|
map(quoted_raw, |q| (q, &b""[..])),
|
|
|
|
)),
|
|
|
|
)(input)?;
|
|
|
|
Ok(ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn sieve_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
|
|
|
crate::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)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
}
|
|
|
|
))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|