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.
memfd
Manos Pitsidianakis 2020-02-28 15:47:07 +02:00
parent 63467a3c45
commit 6079909f9c
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
6 changed files with 301 additions and 24 deletions

View File

@ -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<dyn Fn(&str) -> bool + Send + Sync>);
@ -87,6 +89,7 @@ impl std::ops::Deref for IsSubscribedFn {
}
type Capabilities = FnvHashSet<Vec<u8>>;
#[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(),

View File

@ -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<std::net::TcpStream>,
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<Vec<&[u8]>, _> = 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<()> {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Vec<(&[u8], &[u8])>>,
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<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 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(())
}
}

View File

@ -988,7 +988,7 @@ pub fn quoted(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
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,

View File

@ -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"] }

View File

@ -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::<Vec<String>>();
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(())
}