/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: Apache-2.0 OR MIT */ use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; use std::time::Duration; use encodify::base64::STANDARD as B64; use super::error::{ContainerError, ContainerResult}; pub struct SieveSeed { reader: BufReader, writer: TcpStream, } impl SieveSeed { pub fn connect_seed(host: &str, port: u16) -> ContainerResult { Self::connect(host, port) } pub fn connect(host: &str, port: u16) -> ContainerResult { let stream = TcpStream::connect((host, port))?; stream.set_read_timeout(Some(Duration::from_secs(30)))?; stream.set_write_timeout(Some(Duration::from_secs(30)))?; let writer = stream.try_clone()?; let mut me = Self { reader: BufReader::new(stream), writer, }; me.read_until_ok()?; Ok(me) } fn read_line(&mut self) -> ContainerResult { let mut line = String::new(); let n = self.reader.read_line(&mut line)?; if n == 0 { return Err(ContainerError::Protocol("sieve eof".to_owned())); } if line.ends_with('\n') { line.pop(); if line.ends_with('\r') { line.pop(); } } Ok(line) } fn read_until_ok(&mut self) -> ContainerResult> { let mut lines = Vec::new(); loop { let line = self.read_line()?; if line.starts_with("OK") { return Ok(lines); } if line.starts_with("NO") || line.starts_with("BYE") { return Err(ContainerError::Protocol(format!("sieve: {line}"))); } lines.push(line); } } fn write_all(&mut self, bytes: &[u8]) -> ContainerResult<()> { self.writer.write_all(bytes)?; self.writer.flush()?; Ok(()) } pub fn authenticate(&mut self, user: &str, password: &str) -> ContainerResult<()> { let mut blob = Vec::new(); blob.push(0u8); blob.extend_from_slice(user.as_bytes()); blob.push(0u8); blob.extend_from_slice(password.as_bytes()); let encoded = B64.encode(&blob); let cmd = format!( "AUTHENTICATE \"PLAIN\" {{{}+}}\r\n{encoded}\r\n", encoded.len() ); self.write_all(cmd.as_bytes())?; self.read_until_ok()?; Ok(()) } pub fn putscript(&mut self, name: &str, body: &str) -> ContainerResult<()> { let cmd = format!( "PUTSCRIPT \"{}\" {{{}+}}\r\n{}\r\n", escape(name), body.len(), body ); self.write_all(cmd.as_bytes())?; self.read_until_ok()?; Ok(()) } pub fn putscript_raw(&mut self, name: &str, body: &str) -> ContainerResult { let cmd = format!( "PUTSCRIPT \"{}\" {{{}+}}\r\n{}\r\n", escape(name), body.len(), body ); self.write_all(cmd.as_bytes())?; match self.read_until_ok() { Ok(_) => Ok(true), Err(ContainerError::Protocol(_)) => Ok(false), Err(e) => Err(e), } } pub fn setactive(&mut self, name: &str) -> ContainerResult<()> { self.write_all(format!("SETACTIVE \"{}\"\r\n", escape(name)).as_bytes())?; self.read_until_ok()?; Ok(()) } pub fn listscripts(&mut self) -> ContainerResult> { self.write_all(b"LISTSCRIPTS\r\n")?; let lines = self.read_until_ok()?; let mut scripts = Vec::new(); for line in lines { let trimmed = line.trim(); if let Some(stripped) = trimmed.strip_prefix('"') { let close = stripped.find('"').unwrap_or(stripped.len()); let name = stripped[..close].to_owned(); let rest = &stripped[close..]; let active = rest.contains("ACTIVE"); scripts.push((name, active)); } } Ok(scripts) } pub fn logout(&mut self) -> ContainerResult<()> { let _ = self.write_all(b"LOGOUT\r\n"); let mut buf = String::new(); let _ = self.reader.read_to_string(&mut buf); Ok(()) } } fn escape(s: &str) -> String { s.replace('\\', "\\\\").replace('"', "\\\"") }