/*
* 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 .
*/
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,
};
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> {
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)
}
#[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::>();
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()));
}
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()))
}
impl ManageSieveConnection {
pub fn new(
account_hash: crate::backends::AccountHash,
account_name: String,
s: &AccountSettings,
event_consumer: crate::backends::BackendEventConsumer,
) -> Result {
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 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(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
}
pub async fn listscripts(&mut self) -> Result, 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::, 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(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.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(|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> {
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<()> {
Ok(())
}
}