From 6079909f9c11030cedc35d0d9071d02a32524596 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Fri, 28 Feb 2020 15:47:07 +0200 Subject: [PATCH] imap: add managesieve connection So far only the connection is implemented, and using the testing/manage_sieve binary you can get a shell to a managesieve server. The managesieve interface will be used in the UI from a plugin, but the plugin's interface isn't implemented yet. --- melib/src/backends/imap.rs | 4 + melib/src/backends/imap/connection.rs | 116 ++++++++++++++---- melib/src/backends/imap/managesieve.rs | 130 +++++++++++++++++++++ melib/src/backends/imap/protocol_parser.rs | 2 +- testing/Cargo.toml | 4 + testing/src/managesieve.rs | 69 +++++++++++ 6 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 melib/src/backends/imap/managesieve.rs create mode 100644 testing/src/managesieve.rs diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index c1f8e556..65b2dd14 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -32,6 +32,7 @@ mod connection; pub use connection::*; mod watch; pub use watch::*; +pub mod managesieve; use crate::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext}; use crate::backends::BackendOp; @@ -69,6 +70,7 @@ pub struct ImapServerConf { pub server_port: u16, pub use_starttls: bool, pub danger_accept_invalid_certs: bool, + pub protocol: ImapProtocol, } struct IsSubscribedFn(Box bool + Send + Sync>); @@ -87,6 +89,7 @@ impl std::ops::Deref for IsSubscribedFn { } type Capabilities = FnvHashSet>; +#[macro_export] macro_rules! get_conf_val { ($s:ident[$var:literal]) => { $s.extra.get($var).ok_or_else(|| { @@ -697,6 +700,7 @@ impl ImapType { server_port, use_starttls, danger_accept_invalid_certs, + protocol: ImapProtocol::IMAP, }; let online = Arc::new(Mutex::new(( Instant::now(), diff --git a/melib/src/backends/imap/connection.rs b/melib/src/backends/imap/connection.rs index e384f87d..58eb2857 100644 --- a/melib/src/backends/imap/connection.rs +++ b/melib/src/backends/imap/connection.rs @@ -35,10 +35,17 @@ use std::time::Instant; use super::protocol_parser; use super::{Capabilities, ImapServerConf}; +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ImapProtocol { + IMAP, + ManageSieve, +} + #[derive(Debug)] pub struct ImapStream { cmd_id: usize, stream: native_tls::TlsStream, + protocol: ImapProtocol, } #[derive(Debug)] @@ -79,11 +86,19 @@ impl ImapStream { let mut socket = TcpStream::connect(&addr)?; socket.set_read_timeout(Some(std::time::Duration::new(4, 0)))?; socket.set_write_timeout(Some(std::time::Duration::new(4, 0)))?; - let cmd_id = 0; + let cmd_id = 1; if server_conf.use_starttls { - socket.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())?; - let mut buf = vec![0; 1024]; + match server_conf.protocol { + ImapProtocol::IMAP => { + socket.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())? + } + ImapProtocol::ManageSieve => { + let len = socket.read(&mut buf)?; + debug!(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) }); + debug!(socket.write_all(b"STARTTLS\r\n")?); + } + } let mut response = String::with_capacity(1024); let mut broken = false; let now = std::time::Instant::now(); @@ -91,12 +106,23 @@ impl ImapStream { while now.elapsed().as_secs() < 3 { let len = socket.read(&mut buf)?; response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) }); - if response.starts_with("* OK ") && response.find("\r\n").is_some() { - if let Some(pos) = response.as_bytes().find(b"\r\n") { - response.drain(0..pos + 2); + match server_conf.protocol { + ImapProtocol::IMAP => { + if response.starts_with("* OK ") && response.find("\r\n").is_some() { + if let Some(pos) = response.as_bytes().find(b"\r\n") { + response.drain(0..pos + 2); + } + } + } + ImapProtocol::ManageSieve => { + if response.starts_with("OK ") && response.find("\r\n").is_some() { + response.clear(); + broken = true; + break; + } } } - if response.starts_with("M0 OK") { + if response.starts_with("M1 OK") { broken = true; break; } @@ -134,7 +160,31 @@ impl ImapStream { conn_result? }; let mut res = String::with_capacity(8 * 1024); - let mut ret = ImapStream { cmd_id, stream }; + let mut ret = ImapStream { + cmd_id, + stream, + protocol: server_conf.protocol, + }; + if let ImapProtocol::ManageSieve = server_conf.protocol { + use data_encoding::BASE64; + ret.read_response(&mut res)?; + ret.send_command( + format!( + "AUTHENTICATE \"PLAIN\" \"{}\"", + BASE64.encode( + format!( + "\0{}\0{}", + &server_conf.server_username, &server_conf.server_password + ) + .as_bytes() + ) + ) + .as_bytes(), + )?; + ret.read_response(&mut res)?; + return Ok((Default::default(), ret)); + } + ret.send_command(b"CAPABILITY")?; ret.read_response(&mut res)?; let capabilities: std::result::Result, _> = res @@ -229,7 +279,10 @@ impl ImapStream { } pub fn read_response(&mut self, ret: &mut String) -> Result<()> { - let id = format!("M{} ", self.cmd_id - 1); + let id = match self.protocol { + ImapProtocol::IMAP => format!("M{} ", self.cmd_id - 1), + ImapProtocol::ManageSieve => String::new(), + }; self.read_lines(ret, &id, true) } @@ -292,15 +345,26 @@ impl ImapStream { pub fn send_command(&mut self, command: &[u8]) -> Result<()> { let command = command.trim(); - self.stream.write_all(b"M")?; - self.stream.write_all(self.cmd_id.to_string().as_bytes())?; - self.stream.write_all(b" ")?; - self.cmd_id += 1; + match self.protocol { + ImapProtocol::IMAP => { + self.stream.write_all(b"M")?; + self.stream.write_all(self.cmd_id.to_string().as_bytes())?; + self.stream.write_all(b" ")?; + self.cmd_id += 1; + } + ImapProtocol::ManageSieve => {} + } + self.stream.write_all(command)?; self.stream.write_all(b"\r\n")?; - debug!("sent: M{} {}", self.cmd_id - 1, unsafe { - std::str::from_utf8_unchecked(command) - }); + match self.protocol { + ImapProtocol::IMAP => { + debug!("sent: M{} {}", self.cmd_id - 1, unsafe { + std::str::from_utf8_unchecked(command) + }); + } + ImapProtocol::ManageSieve => {} + } Ok(()) } @@ -365,14 +429,20 @@ impl ImapConnection { pub fn read_response(&mut self, ret: &mut String) -> Result<()> { self.try_send(|s| s.read_response(ret))?; - let r: ImapResponse = ImapResponse::from(&ret); - if let ImapResponse::Bye(ref response_code) = r { - self.stream = Err(MeliError::new(format!( - "Offline: received BYE: {:?}", - response_code - ))); + + match self.server_conf.protocol { + ImapProtocol::IMAP => { + let r: ImapResponse = ImapResponse::from(&ret); + if let ImapResponse::Bye(ref response_code) = r { + self.stream = Err(MeliError::new(format!( + "Offline: received BYE: {:?}", + response_code + ))); + } + r.into() + } + ImapProtocol::ManageSieve => Ok(()), } - r.into() } pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> { diff --git a/melib/src/backends/imap/managesieve.rs b/melib/src/backends/imap/managesieve.rs new file mode 100644 index 00000000..f31489cb --- /dev/null +++ b/melib/src/backends/imap/managesieve.rs @@ -0,0 +1,130 @@ +/* + * 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}; +use crate::conf::AccountSettings; +use crate::error::{MeliError, Result}; +use crate::get_conf_val; +use nom::IResult; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +named!( + pub managesieve_capabilities>, + do_parse!( + ret: separated_nonempty_list_complete!(tag!(b"\r\n"), alt_complete!(separated_pair!(quoted_raw, tag!(b" "), quoted_raw) | map!(quoted_raw, |q| (q, &b""[..])))) + >> opt!(tag!("\r\n")) + >> ({ 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").to_full_result(), Ok(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"[..])]) + +); +} + +// 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 IResult::Error(nom::ErrorKind::Custom(0)); + } + + let mut i = 1; + while i < input.len() { + if input[i] == b'\"' && input[i - 1] != b'\\' { + return IResult::Done(&input[i + 1..], &input[1..i]); + } + i += 1; + } + + return IResult::Error(nom::ErrorKind::Custom(0)); +} + +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<()>; +} + +pub fn new_managesieve_connection(s: &AccountSettings) -> 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 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, + danger_accept_invalid_certs, + protocol: ImapProtocol::ManageSieve, + }; + let online = Arc::new(Mutex::new(( + Instant::now(), + Err(MeliError::new("Account is uninitialised.")), + ))); + Ok(ImapConnection::new_connection(&server_conf, online)) +} + +impl ManageSieve for ImapConnection { + fn havespace(&mut self) -> Result<()> { + Ok(()) + } + fn putscript(&mut self) -> Result<()> { + Ok(()) + } + + fn listscripts(&mut self) -> Result<()> { + Ok(()) + } + fn setactive(&mut self) -> Result<()> { + Ok(()) + } + + fn getscript(&mut self) -> Result<()> { + Ok(()) + } + + fn deletescript(&mut self) -> Result<()> { + Ok(()) + } + fn renamescript(&mut self) -> Result<()> { + Ok(()) + } +} diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index b47ad5a4..9eb2e046 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -988,7 +988,7 @@ pub fn quoted(input: &[u8]) -> IResult<&[u8], Vec> { let mut i = 1; while i < input.len() { - if input[i] == b'\"' && (i == 0 || (input[i - 1] != b'\\')) { + if input[i] == b'\"' && input[i - 1] != b'\\' { return match crate::email::parser::phrase(&input[1..i], false) { IResult::Done(_, out) => IResult::Done(&input[i + 1..], out), e => e, diff --git a/testing/Cargo.toml b/testing/Cargo.toml index a89f0a00..4f5983cf 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -13,6 +13,10 @@ path = "src/email_parse.rs" name = "imapconn" path = "src/imap_conn.rs" +[[bin]] +name = "managesieve_conn" +path = "src/managesieve.rs" + [dependencies] melib = { path = "../melib", version = "*", features = ["debug-tracing", "unicode_algorithms"] } diff --git a/testing/src/managesieve.rs b/testing/src/managesieve.rs new file mode 100644 index 00000000..75d57787 --- /dev/null +++ b/testing/src/managesieve.rs @@ -0,0 +1,69 @@ +extern crate melib; + +use melib::backends::imap::managesieve::new_managesieve_connection; +use melib::AccountSettings; +use melib::Result; + +/// Opens an interactive shell on a managesieve server. Suggested use is with rlwrap(1) +/// +/// # Example invocation: +/// ```sh +/// ./manage_sieve server_hostname server_username server_password server_port"); +/// ``` +/// +/// `danger_accept_invalid_certs` is turned on by default, so no certificate validation is performed. + +fn main() -> Result<()> { + let mut args = std::env::args().skip(1).collect::>(); + if args.len() != 4 { + eprintln!( + "Usage: manage_sieve server_hostname server_username server_password server_port" + ); + std::process::exit(1); + } + + let (a, b, c, d) = ( + std::mem::replace(&mut args[0], String::new()), + std::mem::replace(&mut args[1], String::new()), + std::mem::replace(&mut args[2], String::new()), + std::mem::replace(&mut args[3], String::new()), + ); + let set = AccountSettings { + extra: [ + ("server_hostname".to_string(), a), + ("server_username".to_string(), b), + ("server_password".to_string(), c), + ("server_port".to_string(), d), + ( + "danger_accept_invalid_certs".to_string(), + "true".to_string(), + ), + ] + .iter() + .cloned() + .collect(), + ..Default::default() + }; + let mut conn = new_managesieve_connection(&set)?; + conn.connect()?; + let mut res = String::with_capacity(8 * 1024); + + let mut input = String::new(); + loop { + use std::io; + input.clear(); + match io::stdin().read_line(&mut input) { + Ok(_) => { + if input.trim().eq_ignore_ascii_case("logout") { + break; + } + conn.send_command(input.as_bytes()).unwrap(); + conn.read_lines(&mut res, String::new()).unwrap(); + println!("out: {}", &res); + } + Err(error) => println!("error: {}", error), + } + } + + Ok(()) +}