diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-03-08 08:17:03 +0100 |
commit | 1a43ce5ac7033c148f64a033f2b1d335e95e11d5 (patch) | |
tree | 60b234604170fe207248458a9c4cdd3f4b7c36f2 /src | |
parent | bb9cb386b65834c44cae86bd100f800883022062 (diff) | |
download | aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.tar.gz aerogramme-1a43ce5ac7033c148f64a033f2b1d335e95e11d5.zip |
WIP refactor
Diffstat (limited to 'src')
57 files changed, 0 insertions, 17420 deletions
diff --git a/src/auth.rs b/src/auth.rs deleted file mode 100644 index 064c90c..0000000 --- a/src/auth.rs +++ /dev/null @@ -1,941 +0,0 @@ -use std::net::SocketAddr; - -use anyhow::{anyhow, bail, Result}; -use futures::stream::{FuturesUnordered, StreamExt}; -use tokio::io::BufStream; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::watch; - -use crate::config::AuthConfig; -use crate::login::ArcLoginProvider; - -/// Seek compatibility with the Dovecot Authentication Protocol -/// -/// ## Trace -/// -/// ```text -/// S: VERSION 1 2 -/// S: MECH PLAIN plaintext -/// S: MECH LOGIN plaintext -/// S: SPID 15 -/// S: CUID 17654 -/// S: COOKIE f56692bee41f471ed01bd83520025305 -/// S: DONE -/// C: VERSION 1 2 -/// C: CPID 1 -/// -/// C: AUTH 2 PLAIN service=smtp -/// S: CONT 2 -/// C: CONT 2 base64stringFollowingRFC4616== -/// S: OK 2 user=alice@example.tld -/// -/// C: AUTH 42 LOGIN service=smtp -/// S: CONT 42 VXNlcm5hbWU6 -/// C: CONT 42 b64User -/// S: CONT 42 UGFzc3dvcmQ6 -/// C: CONT 42 b64Pass -/// S: FAIL 42 user=alice -/// ``` -/// -/// ## RFC References -/// -/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616 -/// -/// -/// ## Dovecot References -/// -/// https://doc.dovecot.org/developer_manual/design/auth_protocol/ -/// https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms -/// https://doc.dovecot.org/configuration_manual/howto/simple_virtual_install/#simple-virtual-install-smtp-auth -/// https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/#howto-postfix-and-dovecot-sasl -pub struct AuthServer { - login_provider: ArcLoginProvider, - bind_addr: SocketAddr, -} - -impl AuthServer { - pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self { - Self { - bind_addr: config.bind_addr, - login_provider, - } - } - - pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - tracing::info!( - "SASL Authentication Protocol listening on {:#}", - self.bind_addr - ); - - let mut connections = FuturesUnordered::new(); - - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - - let (socket, remote_addr) = tokio::select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - - tracing::info!("AUTH: accepted connection from {}", remote_addr); - let conn = tokio::spawn( - NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(), - ); - - connections.push(conn); - } - drop(tcp); - - tracing::info!("AUTH server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -struct NetLoop { - login: ArcLoginProvider, - stream: BufStream<TcpStream>, - stop: watch::Receiver<bool>, - state: State, - read_buf: Vec<u8>, - write_buf: BytesMut, -} - -impl NetLoop { - fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver<bool>) -> Self { - Self { - login, - stream: BufStream::new(stream), - state: State::Init, - stop, - read_buf: Vec::new(), - write_buf: BytesMut::new(), - } - } - - async fn run_error(self) { - match self.run().await { - Ok(()) => tracing::info!("Auth session succeeded"), - Err(e) => tracing::error!(err=?e, "Auth session failed"), - } - } - - async fn run(mut self) -> Result<()> { - loop { - tokio::select! { - read_res = self.stream.read_until(b'\n', &mut self.read_buf) => { - // Detect EOF / socket close - let bread = read_res?; - if bread == 0 { - tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session."); - return Ok(()) - } - - // Parse command - let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?; - tracing::trace!(cmd=?cmd, "Received command"); - - // Make some progress in our local state - self.state.progress(cmd, &self.login).await; - if matches!(self.state, State::Error) { - bail!("Internal state is in error, previous logs explain what went wrong"); - } - - // Build response - let srv_cmds = self.state.response(); - srv_cmds.iter().try_for_each(|r| { - tracing::trace!(cmd=?r, "Sent command"); - r.encode(&mut self.write_buf) - })?; - - // Send responses if at least one command response has been generated - if !srv_cmds.is_empty() { - self.stream.write_all(&self.write_buf).await?; - self.stream.flush().await?; - } - - // Reset buffers - self.read_buf.clear(); - self.write_buf.clear(); - }, - _ = self.stop.changed() => { - tracing::debug!("Server is stopping, quitting this runner"); - return Ok(()) - } - } - } - } -} - -// ----------------------------------------------------------------- -// -// BUSINESS LOGIC -// -// ----------------------------------------------------------------- -use rand::prelude::*; - -#[derive(Debug)] -enum AuthRes { - Success(String), - Failed(Option<String>, Option<FailCode>), -} - -#[derive(Debug)] -enum State { - Error, - Init, - HandshakePart(Version), - HandshakeDone, - AuthPlainProgress { id: u64 }, - AuthDone { id: u64, res: AuthRes }, -} - -const SERVER_MAJOR: u64 = 1; -const SERVER_MINOR: u64 = 2; -const EMPTY_AUTHZ: &[u8] = &[]; -impl State { - async fn try_auth_plain<'a>(&self, data: &'a [u8], login: &ArcLoginProvider) -> AuthRes { - // Check that we can extract user's login+pass - let (ubin, pbin) = match auth_plain(&data) { - Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass), - Ok(_) => { - tracing::error!("Impersonating user is not supported"); - return AuthRes::Failed(None, None); - } - Err(e) => { - tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk"); - return AuthRes::Failed(None, None); - } - }; - - // Try to convert it to UTF-8 - let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) { - (Ok(u), Ok(p)) => (u, p), - _ => { - tracing::error!("Username or password contain invalid UTF-8 characters"); - return AuthRes::Failed(None, None); - } - }; - - // Try to connect user - match login.login(user, password).await { - Ok(_) => AuthRes::Success(user.to_string()), - Err(e) => { - tracing::warn!(err=?e, "login failed"); - AuthRes::Failed(Some(user.to_string()), None) - } - } - } - - async fn progress(&mut self, cmd: ClientCommand, login: &ArcLoginProvider) { - let new_state = 'state: { - match (std::mem::replace(self, State::Error), cmd) { - (Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v), - (Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => { - if version.major != SERVER_MAJOR { - tracing::error!( - client_major = version.major, - server_major = SERVER_MAJOR, - "Unsupported client major version" - ); - break 'state Self::Error; - } - - Self::HandshakeDone - } - ( - Self::HandshakeDone { .. }, - ClientCommand::Auth { - id, mech, options, .. - }, - ) - | ( - Self::AuthDone { .. }, - ClientCommand::Auth { - id, mech, options, .. - }, - ) => { - if mech != Mechanism::Plain { - tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism"); - break 'state Self::AuthDone { - id, - res: AuthRes::Failed(None, None), - }; - } - - match options.last() { - Some(AuthOption::Resp(data)) => Self::AuthDone { - id, - res: self.try_auth_plain(&data, login).await, - }, - _ => Self::AuthPlainProgress { id }, - } - } - (Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => { - // Check that ID matches - if cid != id { - tracing::error!( - auth_id = id, - cont_id = cid, - "CONT id does not match AUTH id" - ); - break 'state Self::AuthDone { - id, - res: AuthRes::Failed(None, None), - }; - } - - Self::AuthDone { - id, - res: self.try_auth_plain(&data, login).await, - } - } - _ => { - tracing::error!("This command is not valid in this context"); - Self::Error - } - } - }; - tracing::debug!(state=?new_state, "Made progress"); - *self = new_state; - } - - fn response(&self) -> Vec<ServerCommand> { - let mut srv_cmd: Vec<ServerCommand> = Vec::new(); - - match self { - Self::HandshakeDone { .. } => { - srv_cmd.push(ServerCommand::Version(Version { - major: SERVER_MAJOR, - minor: SERVER_MINOR, - })); - - srv_cmd.push(ServerCommand::Mech { - kind: Mechanism::Plain, - parameters: vec![MechanismParameters::PlainText], - }); - - srv_cmd.push(ServerCommand::Spid(15u64)); - srv_cmd.push(ServerCommand::Cuid(19350u64)); - - let mut cookie = [0u8; 16]; - thread_rng().fill(&mut cookie); - srv_cmd.push(ServerCommand::Cookie(cookie)); - - srv_cmd.push(ServerCommand::Done); - } - Self::AuthPlainProgress { id } => { - srv_cmd.push(ServerCommand::Cont { - id: *id, - data: None, - }); - } - Self::AuthDone { - id, - res: AuthRes::Success(user), - } => { - srv_cmd.push(ServerCommand::Ok { - id: *id, - user_id: Some(user.to_string()), - extra_parameters: vec![], - }); - } - Self::AuthDone { - id, - res: AuthRes::Failed(maybe_user, maybe_failcode), - } => { - srv_cmd.push(ServerCommand::Fail { - id: *id, - user_id: maybe_user.clone(), - code: maybe_failcode.clone(), - extra_parameters: vec![], - }); - } - _ => (), - }; - - srv_cmd - } -} - -// ----------------------------------------------------------------- -// -// DOVECOT AUTH TYPES -// -// ----------------------------------------------------------------- - -#[derive(Debug, Clone, PartialEq)] -enum Mechanism { - Plain, - Login, -} - -#[derive(Clone, Debug)] -enum AuthOption { - /// Unique session ID. Mainly used for logging. - Session(u64), - /// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1. - LocalIp(String), - /// Remote client IP - RemoteIp(String), - /// Local port connected to by the client. - LocalPort(u16), - /// Remote client port - RemotePort(u16), - /// When Dovecot proxy is used, the real_rip/real_port are the proxy’s IP/port and real_lip/real_lport are the backend’s IP/port where the proxy was connected to. - RealRemoteIp(String), - RealLocalIp(String), - RealLocalPort(u16), - RealRemotePort(u16), - /// TLS SNI name - LocalName(String), - /// Enable debugging for this lookup. - Debug, - /// List of fields that will become available via %{forward_*} variables. The list is double-tab-escaped, like: tab_escaped[tab_escaped(key=value)[<TAB>...] - /// Note: we do not unescape the tabulation, and thus we don't parse the data - ForwardViews(Vec<u8>), - /// Remote user has secured transport to auth client (e.g. localhost, SSL, TLS). - Secured(Option<String>), - /// The value can be “insecure”, “trusted” or “TLS”. - Transport(String), - /// TLS cipher being used. - TlsCipher(String), - /// The number of bits in the TLS cipher. - /// @FIXME: I don't know how if it's a string or an integer - TlsCipherBits(String), - /// TLS perfect forward secrecy algorithm (e.g. DH, ECDH) - TlsPfs(String), - /// TLS protocol name (e.g. SSLv3, TLSv1.2) - TlsProtocol(String), - /// Remote user has presented a valid SSL certificate. - ValidClientCert(String), - /// Ignore auth penalty tracking for this request - NoPenalty, - /// Unknown option sent by Postfix - NoLogin, - /// Username taken from client’s SSL certificate. - CertUsername, - /// IMAP ID string - ClientId, - /// An unknown key - UnknownPair(String, Vec<u8>), - UnknownBool(Vec<u8>), - /// Initial response for authentication mechanism. - /// NOTE: This must be the last parameter. Everything after it is ignored. - /// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs. - /// @FIXME: I don't understand this parameter - Resp(Vec<u8>), -} - -#[derive(Debug, Clone)] -struct Version { - major: u64, - minor: u64, -} - -#[derive(Debug)] -enum ClientCommand { - /// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2. - Version(Version), - /// CPID finishes the handshake from client. - Cpid(u64), - Auth { - /// ID is a connection-specific unique request identifier. It must be a 32bit number, so typically you’d just increment it by one. - id: u64, - /// A SASL mechanism (eg. LOGIN, PLAIN, etc.) - /// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms - mech: Mechanism, - /// Service is the service requesting authentication, eg. pop3, imap, smtp. - service: String, - /// All the optional parameters - options: Vec<AuthOption>, - }, - Cont { - /// The <id> must match the <id> of the AUTH command. - id: u64, - /// Data that will be serialized to / deserialized from base64 - data: Vec<u8>, - }, -} - -#[derive(Debug)] -enum MechanismParameters { - /// Anonymous authentication - Anonymous, - /// Transfers plaintext passwords - PlainText, - /// Subject to passive (dictionary) attack - Dictionary, - /// Subject to active (non-dictionary) attack - Active, - /// Provides forward secrecy between sessions - ForwardSecrecy, - /// Provides mutual authentication - MutualAuth, - /// Don’t advertise this as available SASL mechanism (eg. APOP) - Private, -} - -#[derive(Debug, Clone)] -enum FailCode { - /// This is a temporary internal failure, e.g. connection was lost to SQL database. - TempFail, - /// Authentication succeeded, but authorization failed (master user’s password was ok, but destination user was not ok). - AuthzFail, - /// User is disabled (password may or may not have been correct) - UserDisabled, - /// User’s password has expired. - PassExpired, -} - -#[derive(Debug)] -enum ServerCommand { - /// Both client and server should check that they support the same major version number. If they don’t, the other side isn’t expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2. - Version(Version), - /// CPID and SPID specify client and server Process Identifiers (PIDs). They should be unique identifiers for the specific process. UNIX process IDs are good choices. - /// SPID can be used by authentication client to tell master which server process handled the authentication. - Spid(u64), - /// CUID is a server process-specific unique connection identifier. It’s different each time a connection is established for the server. - /// CUID is currently useful only for APOP authentication. - Cuid(u64), - Mech { - kind: Mechanism, - parameters: Vec<MechanismParameters>, - }, - /// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+) - Cookie([u8; 16]), - /// DONE finishes the handshake from server. - Done, - - Fail { - id: u64, - user_id: Option<String>, - code: Option<FailCode>, - extra_parameters: Vec<Vec<u8>>, - }, - Cont { - id: u64, - data: Option<Vec<u8>>, - }, - /// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially. - /// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known. - Ok { - id: u64, - user_id: Option<String>, - extra_parameters: Vec<Vec<u8>>, - }, -} - -// ----------------------------------------------------------------- -// -// DOVECOT AUTH DECODING -// -// ------------------------------------------------------------------ - -use base64::Engine; -use nom::{ - branch::alt, - bytes::complete::{is_not, tag, tag_no_case, take, take_while, take_while1}, - character::complete::{tab, u16, u64}, - combinator::{map, opt, recognize, rest, value}, - error::{Error, ErrorKind}, - multi::{many1, separated_list0}, - sequence::{pair, preceded, tuple}, - IResult, -}; - -fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> { - let mut parser = tuple((tag_no_case(b"VERSION"), tab, u64, tab, u64)); - - let (input, (_, _, major, _, minor)) = parser(input)?; - Ok((input, ClientCommand::Version(Version { major, minor }))) -} - -fn cpid_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> { - preceded( - pair(tag_no_case(b"CPID"), tab), - map(u64, |v| ClientCommand::Cpid(v)), - )(input) -} - -fn mechanism<'a>(input: &'a [u8]) -> IResult<&'a [u8], Mechanism> { - alt(( - value(Mechanism::Plain, tag_no_case(b"PLAIN")), - value(Mechanism::Login, tag_no_case(b"LOGIN")), - ))(input) -} - -fn is_not_tab_or_esc_or_lf(c: u8) -> bool { - c != 0x09 && c != 0x01 && c != 0x0a // TAB or 0x01 or LF -} - -fn is_esc<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> { - preceded(tag(&[0x01]), take(1usize))(input) -} - -fn parameter<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> { - recognize(many1(alt((take_while1(is_not_tab_or_esc_or_lf), is_esc))))(input) -} - -fn parameter_str(input: &[u8]) -> IResult<&[u8], String> { - let (input, buf) = parameter(input)?; - - std::str::from_utf8(buf) - .map(|v| (input, v.to_string())) - .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1))) -} - -fn is_param_name_char(c: u8) -> bool { - is_not_tab_or_esc_or_lf(c) && c != 0x3d // = -} - -fn parameter_name(input: &[u8]) -> IResult<&[u8], String> { - let (input, buf) = take_while1(is_param_name_char)(input)?; - - std::str::from_utf8(buf) - .map(|v| (input, v.to_string())) - .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1))) -} - -fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> { - preceded(tag_no_case("service="), parameter_str)(input) -} - -fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> { - use AuthOption::*; - alt(( - alt(( - value(Debug, tag_no_case(b"debug")), - value(NoPenalty, tag_no_case(b"no-penalty")), - value(ClientId, tag_no_case(b"client_id")), - value(NoLogin, tag_no_case(b"nologin")), - map(preceded(tag_no_case(b"session="), u64), |id| Session(id)), - map(preceded(tag_no_case(b"lip="), parameter_str), |ip| { - LocalIp(ip) - }), - map(preceded(tag_no_case(b"rip="), parameter_str), |ip| { - RemoteIp(ip) - }), - map(preceded(tag_no_case(b"lport="), u16), |port| { - LocalPort(port) - }), - map(preceded(tag_no_case(b"rport="), u16), |port| { - RemotePort(port) - }), - map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| { - RealRemoteIp(ip) - }), - map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| { - RealLocalIp(ip) - }), - map(preceded(tag_no_case(b"real_lport="), u16), |port| { - RealLocalPort(port) - }), - map(preceded(tag_no_case(b"real_rport="), u16), |port| { - RealRemotePort(port) - }), - )), - alt(( - map( - preceded(tag_no_case(b"local_name="), parameter_str), - |name| LocalName(name), - ), - map( - preceded(tag_no_case(b"forward_views="), parameter), - |views| ForwardViews(views.into()), - ), - map(preceded(tag_no_case(b"secured="), parameter_str), |info| { - Secured(Some(info)) - }), - value(Secured(None), tag_no_case(b"secured")), - value(CertUsername, tag_no_case(b"cert_username")), - map(preceded(tag_no_case(b"transport="), parameter_str), |ts| { - Transport(ts) - }), - map( - preceded(tag_no_case(b"tls_cipher="), parameter_str), - |cipher| TlsCipher(cipher), - ), - map( - preceded(tag_no_case(b"tls_cipher_bits="), parameter_str), - |bits| TlsCipherBits(bits), - ), - map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| { - TlsPfs(pfs) - }), - map( - preceded(tag_no_case(b"tls_protocol="), parameter_str), - |proto| TlsProtocol(proto), - ), - map( - preceded(tag_no_case(b"valid-client-cert="), parameter_str), - |cert| ValidClientCert(cert), - ), - )), - alt(( - map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)), - map( - tuple((parameter_name, tag(b"="), parameter)), - |(n, _, v)| UnknownPair(n, v.into()), - ), - map(parameter, |v| UnknownBool(v.into())), - )), - ))(input) -} - -fn auth_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> { - let mut parser = tuple(( - tag_no_case(b"AUTH"), - tab, - u64, - tab, - mechanism, - tab, - service, - map(opt(preceded(tab, separated_list0(tab, auth_option))), |o| { - o.unwrap_or(vec![]) - }), - )); - let (input, (_, _, id, _, mech, _, service, options)) = parser(input)?; - Ok(( - input, - ClientCommand::Auth { - id, - mech, - service, - options, - }, - )) -} - -fn is_base64_core(c: u8) -> bool { - c >= 0x30 && c <= 0x39 // 0-9 - || c >= 0x41 && c <= 0x5a // A-Z - || c >= 0x61 && c <= 0x7a // a-z - || c == 0x2b // + - || c == 0x2f // / -} - -fn is_base64_pad(c: u8) -> bool { - c == 0x3d // = -} - -fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> { - let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?; - - let data = base64::engine::general_purpose::STANDARD_NO_PAD - .decode(b64) - .map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?; - - Ok((input, data)) -} - -/// @FIXME Dovecot does not say if base64 content must be padded or not -fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> { - let mut parser = tuple((tag_no_case(b"CONT"), tab, u64, tab, base64)); - - let (input, (_, _, id, _, data)) = parser(input)?; - Ok((input, ClientCommand::Cont { id, data })) -} - -fn client_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> { - alt((version_command, cpid_command, auth_command, cont_command))(input) -} - -/* -fn server_command(buf: &u8) -> IResult<&u8, ServerCommand> { - unimplemented!(); -} -*/ - -// ----------------------------------------------------------------- -// -// SASL DECODING -// -// ----------------------------------------------------------------- - -fn not_null(c: u8) -> bool { - c != 0x0 -} - -// impersonated user, login, password -fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> { - map( - tuple(( - take_while(not_null), - take(1usize), - take_while(not_null), - take(1usize), - rest, - )), - |(imp, _, user, _, pass)| (imp, user, pass), - )(input) -} - -// ----------------------------------------------------------------- -// -// DOVECOT AUTH ENCODING -// -// ------------------------------------------------------------------ -use tokio_util::bytes::{BufMut, BytesMut}; -trait Encode { - fn encode(&self, out: &mut BytesMut) -> Result<()>; -} - -fn tab_enc(out: &mut BytesMut) { - out.put(&[0x09][..]) -} - -fn lf_enc(out: &mut BytesMut) { - out.put(&[0x0A][..]) -} - -impl Encode for Mechanism { - fn encode(&self, out: &mut BytesMut) -> Result<()> { - match self { - Self::Plain => out.put(&b"PLAIN"[..]), - Self::Login => out.put(&b"LOGIN"[..]), - } - Ok(()) - } -} - -impl Encode for MechanismParameters { - fn encode(&self, out: &mut BytesMut) -> Result<()> { - match self { - Self::Anonymous => out.put(&b"anonymous"[..]), - Self::PlainText => out.put(&b"plaintext"[..]), - Self::Dictionary => out.put(&b"dictionary"[..]), - Self::Active => out.put(&b"active"[..]), - Self::ForwardSecrecy => out.put(&b"forward-secrecy"[..]), - Self::MutualAuth => out.put(&b"mutual-auth"[..]), - Self::Private => out.put(&b"private"[..]), - } - Ok(()) - } -} - -impl Encode for FailCode { - fn encode(&self, out: &mut BytesMut) -> Result<()> { - match self { - Self::TempFail => out.put(&b"temp_fail"[..]), - Self::AuthzFail => out.put(&b"authz_fail"[..]), - Self::UserDisabled => out.put(&b"user_disabled"[..]), - Self::PassExpired => out.put(&b"pass_expired"[..]), - }; - Ok(()) - } -} - -impl Encode for ServerCommand { - fn encode(&self, out: &mut BytesMut) -> Result<()> { - match self { - Self::Version(Version { major, minor }) => { - out.put(&b"VERSION"[..]); - tab_enc(out); - out.put(major.to_string().as_bytes()); - tab_enc(out); - out.put(minor.to_string().as_bytes()); - lf_enc(out); - } - Self::Spid(pid) => { - out.put(&b"SPID"[..]); - tab_enc(out); - out.put(pid.to_string().as_bytes()); - lf_enc(out); - } - Self::Cuid(pid) => { - out.put(&b"CUID"[..]); - tab_enc(out); - out.put(pid.to_string().as_bytes()); - lf_enc(out); - } - Self::Cookie(cval) => { - out.put(&b"COOKIE"[..]); - tab_enc(out); - out.put(hex::encode(cval).as_bytes()); - lf_enc(out); - } - Self::Mech { kind, parameters } => { - out.put(&b"MECH"[..]); - tab_enc(out); - kind.encode(out)?; - for p in parameters.iter() { - tab_enc(out); - p.encode(out)?; - } - lf_enc(out); - } - Self::Done => { - out.put(&b"DONE"[..]); - lf_enc(out); - } - Self::Cont { id, data } => { - out.put(&b"CONT"[..]); - tab_enc(out); - out.put(id.to_string().as_bytes()); - tab_enc(out); - if let Some(rdata) = data { - let b64 = base64::engine::general_purpose::STANDARD.encode(rdata); - out.put(b64.as_bytes()); - } - lf_enc(out); - } - Self::Ok { - id, - user_id, - extra_parameters, - } => { - out.put(&b"OK"[..]); - tab_enc(out); - out.put(id.to_string().as_bytes()); - if let Some(user) = user_id { - tab_enc(out); - out.put(&b"user="[..]); - out.put(user.as_bytes()); - } - for p in extra_parameters.iter() { - tab_enc(out); - out.put(&p[..]); - } - lf_enc(out); - } - Self::Fail { - id, - user_id, - code, - extra_parameters, - } => { - out.put(&b"FAIL"[..]); - tab_enc(out); - out.put(id.to_string().as_bytes()); - if let Some(user) = user_id { - tab_enc(out); - out.put(&b"user="[..]); - out.put(user.as_bytes()); - } - if let Some(code_val) = code { - tab_enc(out); - out.put(&b"code="[..]); - code_val.encode(out)?; - } - for p in extra_parameters.iter() { - tab_enc(out); - out.put(&p[..]); - } - lf_enc(out); - } - } - Ok(()) - } -} diff --git a/src/bayou.rs b/src/bayou.rs deleted file mode 100644 index 9faff5a..0000000 --- a/src/bayou.rs +++ /dev/null @@ -1,514 +0,0 @@ -use std::sync::{Arc, Weak}; -use std::time::{Duration, Instant}; - -use anyhow::{anyhow, bail, Result}; -use log::error; -use rand::prelude::*; -use serde::{Deserialize, Serialize}; -use tokio::sync::{watch, Notify}; - -use crate::cryptoblob::*; -use crate::login::Credentials; -use crate::storage; -use crate::timestamp::*; - -const KEEP_STATE_EVERY: usize = 64; - -// Checkpointing interval constants: a checkpoint is not made earlier -// than CHECKPOINT_INTERVAL time after the last one, and is not made -// if there are less than CHECKPOINT_MIN_OPS new operations since last one. -const CHECKPOINT_INTERVAL: Duration = Duration::from_secs(6 * 3600); -const CHECKPOINT_MIN_OPS: usize = 16; -// HYPOTHESIS: processes are able to communicate in a synchronous -// fashion in times that are small compared to CHECKPOINT_INTERVAL. -// More precisely, if a process tried to save an operation within the last -// CHECKPOINT_INTERVAL, we are sure to read it from storage if it was -// successfully saved (and if we don't read it, it means it has been -// definitely discarded due to an error). - -// Keep at least two checkpoints, here three, to avoid race conditions -// between processes doing .checkpoint() and those doing .sync() -const CHECKPOINTS_TO_KEEP: usize = 3; - -const WATCH_SK: &str = "watch"; - -pub trait BayouState: - Default + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static -{ - type Op: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + Send + Sync + 'static; - - fn apply(&self, op: &Self::Op) -> Self; -} - -pub struct Bayou<S: BayouState> { - path: String, - key: Key, - - storage: storage::Store, - - checkpoint: (Timestamp, S), - history: Vec<(Timestamp, S::Op, Option<S>)>, - - last_sync: Option<Instant>, - last_try_checkpoint: Option<Instant>, - - watch: Arc<K2vWatch>, - last_sync_watch_ct: storage::RowRef, -} - -impl<S: BayouState> Bayou<S> { - pub async fn new(creds: &Credentials, path: String) -> Result<Self> { - let storage = creds.storage.build().await?; - - //let target = k2v_client.row(&path, WATCH_SK); - let target = storage::RowRef::new(&path, WATCH_SK); - let watch = K2vWatch::new(creds, target.clone()).await?; - - Ok(Self { - path, - storage, - key: creds.keys.master.clone(), - checkpoint: (Timestamp::zero(), S::default()), - history: vec![], - last_sync: None, - last_try_checkpoint: None, - watch, - last_sync_watch_ct: target, - }) - } - - /// Re-reads the state from persistent storage backend - pub async fn sync(&mut self) -> Result<()> { - let new_last_sync = Some(Instant::now()); - let new_last_sync_watch_ct = self.watch.rx.borrow().clone(); - - // 1. List checkpoints - let checkpoints = self.list_checkpoints().await?; - tracing::debug!("(sync) listed checkpoints: {:?}", checkpoints); - - // 2. Load last checkpoint if different from currently used one - let checkpoint = if let Some((ts, key)) = checkpoints.last() { - if *ts == self.checkpoint.0 { - (*ts, None) - } else { - tracing::debug!("(sync) loading checkpoint: {}", key); - - let buf = self - .storage - .blob_fetch(&storage::BlobRef(key.to_string())) - .await? - .value; - tracing::debug!("(sync) checkpoint body length: {}", buf.len()); - - let ck = open_deserialize::<S>(&buf, &self.key)?; - (*ts, Some(ck)) - } - } else { - (Timestamp::zero(), None) - }; - - if self.checkpoint.0 > checkpoint.0 { - bail!("Loaded checkpoint is more recent than stored one"); - } - - if let Some(ck) = checkpoint.1 { - tracing::debug!( - "(sync) updating checkpoint to loaded state at {:?}", - checkpoint.0 - ); - self.checkpoint = (checkpoint.0, ck); - }; - - // remove from history events before checkpoint - self.history = std::mem::take(&mut self.history) - .into_iter() - .skip_while(|(ts, _, _)| *ts < self.checkpoint.0) - .collect(); - - // 3. List all operations starting from checkpoint - let ts_ser = self.checkpoint.0.to_string(); - tracing::debug!("(sync) looking up operations starting at {}", ts_ser); - let ops_map = self - .storage - .row_fetch(&storage::Selector::Range { - shard: &self.path, - sort_begin: &ts_ser, - sort_end: WATCH_SK, - }) - .await?; - - let mut ops = vec![]; - for row_value in ops_map { - let row = row_value.row_ref; - let sort_key = row.uid.sort; - let ts = sort_key - .parse::<Timestamp>() - .map_err(|_| anyhow!("Invalid operation timestamp: {}", sort_key))?; - - let val = row_value.value; - if val.len() != 1 { - bail!("Invalid operation, has {} values", val.len()); - } - match &val[0] { - storage::Alternative::Value(v) => { - let op = open_deserialize::<S::Op>(v, &self.key)?; - tracing::trace!("(sync) operation {}: {:?}", sort_key, op); - ops.push((ts, op)); - } - storage::Alternative::Tombstone => { - continue; - } - } - } - ops.sort_by_key(|(ts, _)| *ts); - tracing::debug!("(sync) {} operations", ops.len()); - - if ops.len() < self.history.len() { - bail!("Some operations have disappeared from storage!"); - } - - // 4. Check that first operation has same timestamp as checkpoint (if not zero) - if self.checkpoint.0 != Timestamp::zero() && ops[0].0 != self.checkpoint.0 { - bail!( - "First operation in listing doesn't have timestamp that corresponds to checkpoint" - ); - } - - // 5. Apply all operations in order - // Hypothesis: before the loaded checkpoint, operations haven't changed - // between what's on storage and what we used to calculate the state in RAM here. - let i0 = self - .history - .iter() - .zip(ops.iter()) - .take_while(|((ts1, _, _), (ts2, _))| ts1 == ts2) - .count(); - - if ops.len() > i0 { - // Remove operations from first position where histories differ - self.history.truncate(i0); - - // Look up last calculated state which we have saved and start from there. - let mut last_state = (0, &self.checkpoint.1); - for (i, (_, _, state_opt)) in self.history.iter().enumerate().rev() { - if let Some(state) = state_opt { - last_state = (i + 1, state); - break; - } - } - - // Calculate state at the end of this common part of the history - let mut state = last_state.1.clone(); - for (_, op, _) in self.history[last_state.0..].iter() { - state = state.apply(op); - } - - // Now, apply all operations retrieved from storage after the common part - for (ts, op) in ops.drain(i0..) { - state = state.apply(&op); - if (self.history.len() + 1) % KEEP_STATE_EVERY == 0 { - self.history.push((ts, op, Some(state.clone()))); - } else { - self.history.push((ts, op, None)); - } - } - - // Always save final state as result of last operation - self.history.last_mut().unwrap().2 = Some(state); - } - - // Save info that sync has been done - self.last_sync = new_last_sync; - self.last_sync_watch_ct = new_last_sync_watch_ct; - Ok(()) - } - - /// Does a sync() if either of the two conditions is met: - /// - last sync was more than CHECKPOINT_INTERVAL/5 ago - /// - a change was detected - pub async fn opportunistic_sync(&mut self) -> Result<()> { - let too_old = match self.last_sync { - Some(t) => Instant::now() > t + (CHECKPOINT_INTERVAL / 5), - _ => true, - }; - let changed = self.last_sync_watch_ct != *self.watch.rx.borrow(); - if too_old || changed { - self.sync().await?; - } - Ok(()) - } - - pub fn notifier(&self) -> std::sync::Weak<Notify> { - Arc::downgrade(&self.watch.learnt_remote_update) - } - - /// Applies a new operation on the state. Once this function returns, - /// the operation has been safely persisted to storage backend. - /// Make sure to call `.opportunistic_sync()` before doing this, - /// and even before calculating the `op` argument given here. - pub async fn push(&mut self, op: S::Op) -> Result<()> { - tracing::debug!("(push) add operation: {:?}", op); - - let ts = Timestamp::after( - self.history - .last() - .map(|(ts, _, _)| ts) - .unwrap_or(&self.checkpoint.0), - ); - - let row_val = storage::RowVal::new( - storage::RowRef::new(&self.path, &ts.to_string()), - seal_serialize(&op, &self.key)?, - ); - self.storage.row_insert(vec![row_val]).await?; - self.watch.propagate_local_update.notify_one(); - - let new_state = self.state().apply(&op); - self.history.push((ts, op, Some(new_state))); - - // Clear previously saved state in history if not required - let hlen = self.history.len(); - if hlen >= 2 && (hlen - 1) % KEEP_STATE_EVERY != 0 { - self.history[hlen - 2].2 = None; - } - - self.checkpoint().await?; - - Ok(()) - } - - /// Save a new checkpoint if previous checkpoint is too old - pub async fn checkpoint(&mut self) -> Result<()> { - match self.last_try_checkpoint { - Some(ts) if Instant::now() - ts < CHECKPOINT_INTERVAL / 5 => Ok(()), - _ => { - let res = self.checkpoint_internal().await; - if res.is_ok() { - self.last_try_checkpoint = Some(Instant::now()); - } - res - } - } - } - - async fn checkpoint_internal(&mut self) -> Result<()> { - self.sync().await?; - - // Check what would be the possible time for a checkpoint in the history we have - let now = now_msec() as i128; - let i_cp = match self - .history - .iter() - .enumerate() - .rev() - .skip_while(|(_, (ts, _, _))| { - (now - ts.msec as i128) < CHECKPOINT_INTERVAL.as_millis() as i128 - }) - .map(|(i, _)| i) - .next() - { - Some(i) => i, - None => { - tracing::debug!("(cp) Oldest operation is too recent to trigger checkpoint"); - return Ok(()); - } - }; - - if i_cp < CHECKPOINT_MIN_OPS { - tracing::debug!("(cp) Not enough old operations to trigger checkpoint"); - return Ok(()); - } - - let ts_cp = self.history[i_cp].0; - tracing::debug!( - "(cp) we could checkpoint at time {} (index {} in history)", - ts_cp.to_string(), - i_cp - ); - - // Check existing checkpoints: if last one is too recent, don't checkpoint again. - let existing_checkpoints = self.list_checkpoints().await?; - tracing::debug!("(cp) listed checkpoints: {:?}", existing_checkpoints); - - if let Some(last_cp) = existing_checkpoints.last() { - if (ts_cp.msec as i128 - last_cp.0.msec as i128) - < CHECKPOINT_INTERVAL.as_millis() as i128 - { - tracing::debug!( - "(cp) last checkpoint is too recent: {}, not checkpointing", - last_cp.0.to_string() - ); - return Ok(()); - } - } - - tracing::debug!("(cp) saving checkpoint at {}", ts_cp.to_string()); - - // Calculate state at time of checkpoint - let mut last_known_state = (0, &self.checkpoint.1); - for (i, (_, _, st)) in self.history[..i_cp].iter().enumerate() { - if let Some(s) = st { - last_known_state = (i + 1, s); - } - } - let mut state_cp = last_known_state.1.clone(); - for (_, op, _) in self.history[last_known_state.0..i_cp].iter() { - state_cp = state_cp.apply(op); - } - - // Serialize and save checkpoint - let cryptoblob = seal_serialize(&state_cp, &self.key)?; - tracing::debug!("(cp) checkpoint body length: {}", cryptoblob.len()); - - let blob_val = storage::BlobVal::new( - storage::BlobRef(format!("{}/checkpoint/{}", self.path, ts_cp.to_string())), - cryptoblob.into(), - ); - self.storage.blob_insert(blob_val).await?; - - // Drop old checkpoints (but keep at least CHECKPOINTS_TO_KEEP of them) - let ecp_len = existing_checkpoints.len(); - if ecp_len + 1 > CHECKPOINTS_TO_KEEP { - let last_to_keep = ecp_len + 1 - CHECKPOINTS_TO_KEEP; - - // Delete blobs - for (_ts, key) in existing_checkpoints[..last_to_keep].iter() { - tracing::debug!("(cp) drop old checkpoint {}", key); - self.storage - .blob_rm(&storage::BlobRef(key.to_string())) - .await?; - } - - // Delete corresponding range of operations - let ts_ser = existing_checkpoints[last_to_keep].0.to_string(); - self.storage - .row_rm(&storage::Selector::Range { - shard: &self.path, - sort_begin: "", - sort_end: &ts_ser, - }) - .await? - } - - Ok(()) - } - - pub fn state(&self) -> &S { - if let Some(last) = self.history.last() { - last.2.as_ref().unwrap() - } else { - &self.checkpoint.1 - } - } - - // ---- INTERNAL ---- - - async fn list_checkpoints(&self) -> Result<Vec<(Timestamp, String)>> { - let prefix = format!("{}/checkpoint/", self.path); - - let checkpoints_res = self.storage.blob_list(&prefix).await?; - - let mut checkpoints = vec![]; - for object in checkpoints_res { - let key = object.0; - if let Some(ckid) = key.strip_prefix(&prefix) { - if let Ok(ts) = ckid.parse::<Timestamp>() { - checkpoints.push((ts, key.into())); - } - } - } - checkpoints.sort_by_key(|(ts, _)| *ts); - Ok(checkpoints) - } -} - -// ---- Bayou watch in K2V ---- - -struct K2vWatch { - target: storage::RowRef, - rx: watch::Receiver<storage::RowRef>, - propagate_local_update: Notify, - learnt_remote_update: Arc<Notify>, -} - -impl K2vWatch { - /// Creates a new watch and launches subordinate threads. - /// These threads hold Weak pointers to the struct; - /// they exit when the Arc is dropped. - async fn new(creds: &Credentials, target: storage::RowRef) -> Result<Arc<Self>> { - let storage = creds.storage.build().await?; - - let (tx, rx) = watch::channel::<storage::RowRef>(target.clone()); - let propagate_local_update = Notify::new(); - let learnt_remote_update = Arc::new(Notify::new()); - - let watch = Arc::new(K2vWatch { - target, - rx, - propagate_local_update, - learnt_remote_update, - }); - - tokio::spawn(Self::background_task(Arc::downgrade(&watch), storage, tx)); - - Ok(watch) - } - - async fn background_task( - self_weak: Weak<Self>, - storage: storage::Store, - tx: watch::Sender<storage::RowRef>, - ) { - let (mut row, remote_update) = match Weak::upgrade(&self_weak) { - Some(this) => (this.target.clone(), this.learnt_remote_update.clone()), - None => return, - }; - - while let Some(this) = Weak::upgrade(&self_weak) { - tracing::debug!( - "bayou k2v watch bg loop iter ({}, {})", - this.target.uid.shard, - this.target.uid.sort - ); - tokio::select!( - // Needed to exit: will force a loop iteration every minutes, - // that will stop the loop if other Arc references have been dropped - // and free resources. Otherwise we would be blocked waiting forever... - _ = tokio::time::sleep(Duration::from_secs(60)) => continue, - - // Watch if another instance has modified the log - update = storage.row_poll(&row) => { - match update { - Err(e) => { - error!("Error in bayou k2v wait value changed: {}", e); - tokio::time::sleep(Duration::from_secs(30)).await; - } - Ok(new_value) => { - row = new_value.row_ref; - if let Err(e) = tx.send(row.clone()) { - tracing::warn!(err=?e, "(watch) can't record the new log ref"); - break; - } - tracing::debug!(row=?row, "(watch) learnt remote update"); - this.learnt_remote_update.notify_waiters(); - } - } - } - - // It appears we have modified the log, informing other people - _ = this.propagate_local_update.notified() => { - let rand = u128::to_be_bytes(thread_rng().gen()).to_vec(); - let row_val = storage::RowVal::new(row.clone(), rand); - if let Err(e) = storage.row_insert(vec![row_val]).await - { - tracing::error!("Error in bayou k2v watch updater loop: {}", e); - tokio::time::sleep(Duration::from_secs(30)).await; - } - } - ); - } - // unblock listeners - remote_update.notify_waiters(); - tracing::info!("bayou k2v watch bg loop exiting"); - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 7de2eac..0000000 --- a/src/config.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::collections::HashMap; -use std::io::{Read, Write}; -use std::net::SocketAddr; -use std::path::PathBuf; - -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CompanionConfig { - pub pid: Option<PathBuf>, - pub imap: ImapUnsecureConfig, - // @FIXME Add DAV - - #[serde(flatten)] - pub users: LoginStaticConfig, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ProviderConfig { - pub pid: Option<PathBuf>, - pub imap: Option<ImapConfig>, - pub imap_unsecure: Option<ImapUnsecureConfig>, - pub lmtp: Option<LmtpConfig>, - pub auth: Option<AuthConfig>, - pub dav_unsecure: Option<DavUnsecureConfig>, - pub users: UserManagement, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(tag = "user_driver")] -pub enum UserManagement { - Demo, - Static(LoginStaticConfig), - Ldap(LoginLdapConfig), -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AuthConfig { - pub bind_addr: SocketAddr, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct LmtpConfig { - pub bind_addr: SocketAddr, - pub hostname: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ImapConfig { - pub bind_addr: SocketAddr, - pub certs: PathBuf, - pub key: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct DavUnsecureConfig { - pub bind_addr: SocketAddr, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ImapUnsecureConfig { - pub bind_addr: SocketAddr, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct LoginStaticConfig { - pub user_list: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(tag = "storage_driver")] -pub enum LdapStorage { - Garage(LdapGarageConfig), - InMemory, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct LdapGarageConfig { - pub s3_endpoint: String, - pub k2v_endpoint: String, - pub aws_region: String, - - pub aws_access_key_id_attr: String, - pub aws_secret_access_key_attr: String, - pub bucket_attr: Option<String>, - pub default_bucket: Option<String>, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct LoginLdapConfig { - // LDAP connection info - pub ldap_server: String, - #[serde(default)] - pub pre_bind_on_login: bool, - pub bind_dn: Option<String>, - pub bind_password: Option<String>, - pub search_base: String, - - // Schema-like info required for Aerogramme's logic - pub username_attr: String, - #[serde(default = "default_mail_attr")] - pub mail_attr: String, - - // The field that will contain the crypto root thingy - pub crypto_root_attr: String, - - // Storage related thing - #[serde(flatten)] - pub storage: LdapStorage, -} - -// ---- - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(tag = "storage_driver")] -pub enum StaticStorage { - Garage(StaticGarageConfig), - InMemory, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct StaticGarageConfig { - pub s3_endpoint: String, - pub k2v_endpoint: String, - pub aws_region: String, - - pub aws_access_key_id: String, - pub aws_secret_access_key: String, - pub bucket: String, -} - -pub type UserList = HashMap<String, UserEntry>; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct UserEntry { - #[serde(default)] - pub email_addresses: Vec<String>, - pub password: String, - pub crypto_root: String, - - #[serde(flatten)] - pub storage: StaticStorage, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct SetupEntry { - #[serde(default)] - pub email_addresses: Vec<String>, - - #[serde(default)] - pub clear_password: Option<String>, - - #[serde(flatten)] - pub storage: StaticStorage, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(tag = "role")] -pub enum AnyConfig { - Companion(CompanionConfig), - Provider(ProviderConfig), -} - -// --- -pub fn read_config<T: serde::de::DeserializeOwned>(config_file: PathBuf) -> Result<T> { - let mut file = std::fs::OpenOptions::new() - .read(true) - .open(config_file.as_path())?; - - let mut config = String::new(); - file.read_to_string(&mut config)?; - - Ok(toml::from_str(&config)?) -} - -pub fn write_config<T: Serialize>(config_file: PathBuf, config: &T) -> Result<()> { - let mut file = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(config_file.as_path())?; - - file.write_all(toml::to_string(config)?.as_bytes())?; - - Ok(()) -} - -fn default_mail_attr() -> String { - "mail".into() -} diff --git a/src/cryptoblob.rs b/src/cryptoblob.rs deleted file mode 100644 index 327a642..0000000 --- a/src/cryptoblob.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Helper functions for secret-key encrypted blobs -//! that contain Zstd encrypted data - -use anyhow::{anyhow, Result}; -use serde::{Deserialize, Serialize}; -use zstd::stream::{decode_all as zstd_decode, encode_all as zstd_encode}; - -//use sodiumoxide::crypto::box_ as publicbox; -use sodiumoxide::crypto::secretbox::xsalsa20poly1305 as secretbox; - -pub use sodiumoxide::crypto::box_::{ - gen_keypair, PublicKey, SecretKey, PUBLICKEYBYTES, SECRETKEYBYTES, -}; -pub use sodiumoxide::crypto::secretbox::xsalsa20poly1305::{gen_key, Key, KEYBYTES}; - -pub fn open(cryptoblob: &[u8], key: &Key) -> Result<Vec<u8>> { - use secretbox::{Nonce, NONCEBYTES}; - - if cryptoblob.len() < NONCEBYTES { - return Err(anyhow!("Cyphertext too short")); - } - - // Decrypt -> get Zstd data - let nonce = Nonce::from_slice(&cryptoblob[..NONCEBYTES]).unwrap(); - let zstdblob = secretbox::open(&cryptoblob[NONCEBYTES..], &nonce, key) - .map_err(|_| anyhow!("Could not decrypt blob"))?; - - // Decompress zstd data - let mut reader = &zstdblob[..]; - let data = zstd_decode(&mut reader)?; - - Ok(data) -} - -pub fn seal(plainblob: &[u8], key: &Key) -> Result<Vec<u8>> { - use secretbox::{gen_nonce, NONCEBYTES}; - - // Compress data using zstd - let mut reader = plainblob; - let zstdblob = zstd_encode(&mut reader, 0)?; - - // Encrypt - let nonce = gen_nonce(); - let cryptoblob = secretbox::seal(&zstdblob, &nonce, key); - - let mut res = Vec::with_capacity(NONCEBYTES + cryptoblob.len()); - res.extend(nonce.as_ref()); - res.extend(cryptoblob); - - Ok(res) -} - -pub fn open_deserialize<T: for<'de> Deserialize<'de>>(cryptoblob: &[u8], key: &Key) -> Result<T> { - let blob = open(cryptoblob, key)?; - - Ok(rmp_serde::decode::from_read_ref::<_, T>(&blob)?) -} - -pub fn seal_serialize<T: Serialize>(obj: T, key: &Key) -> Result<Vec<u8>> { - let mut wr = Vec::with_capacity(128); - let mut se = rmp_serde::Serializer::new(&mut wr) - .with_struct_map() - .with_string_variants(); - obj.serialize(&mut se)?; - - seal(&wr, key) -} diff --git a/src/dav/acltypes.rs b/src/dav/acltypes.rs deleted file mode 100644 index f356813..0000000 --- a/src/dav/acltypes.rs +++ /dev/null @@ -1,4 +0,0 @@ -//@FIXME required for a full DAV implementation -// See section 6. of the CalDAV RFC -// It seems mainly required for free-busy that I will not implement now. -// It can also be used for discovering main calendar, not sure it is used. diff --git a/src/dav/caldecoder.rs b/src/dav/caldecoder.rs deleted file mode 100644 index 5f40c4b..0000000 --- a/src/dav/caldecoder.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::types as dav; -use super::caltypes::*; -use super::xml; -use super::error; - -// ---- ROOT ELEMENTS --- - -// ---- EXTENSIONS --- -impl xml::QRead<Violation> for Violation { - async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { - unreachable!(); - } -} - -impl xml::QRead<Property> for Property { - async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { - unreachable!(); - } -} - -impl xml::QRead<PropertyRequest> for PropertyRequest { - async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { - unreachable!(); - } -} - -impl xml::QRead<ResourceType> for ResourceType { - async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { - unreachable!(); - } -} - -// ---- INNER XML ---- diff --git a/src/dav/calencoder.rs b/src/dav/calencoder.rs deleted file mode 100644 index 58b88c7..0000000 --- a/src/dav/calencoder.rs +++ /dev/null @@ -1,886 +0,0 @@ -use quick_xml::Error as QError; -use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText}; -use quick_xml::name::PrefixDeclaration; -use tokio::io::AsyncWrite; - -use super::caltypes::*; -use super::xml::{Node, QWrite, IWrite, Writer}; -use super::types::Extension; - -const ICAL_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ"; - -// ==================== Calendar Types Serialization ========================= - -// -------------------- MKCALENDAR METHOD ------------------------------------ -impl<E: Extension> QWrite for MkCalendar<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_cal_element("mkcalendar"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension, N: Node<N>> QWrite for MkCalendarResponse<E,N> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_cal_element("mkcalendar-response"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for propstat in self.0.iter() { - propstat.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -// ----------------------- REPORT METHOD ------------------------------------- - -impl<E: Extension> QWrite for CalendarQuery<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_cal_element("calendar-query"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - if let Some(selector) = &self.selector { - selector.qwrite(xml).await?; - } - self.filter.qwrite(xml).await?; - if let Some(tz) = &self.timezone { - tz.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension> QWrite for CalendarMultiget<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_cal_element("calendar-multiget"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - if let Some(selector) = &self.selector { - selector.qwrite(xml).await?; - } - for href in self.href.iter() { - href.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for FreeBusyQuery { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_cal_element("free-busy-query"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -// -------------------------- DAV::prop -------------------------------------- -impl QWrite for PropertyRequest { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut atom = async |c| { - let empty_tag = xml.create_cal_element(c); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }; - - match self { - Self::CalendarDescription => atom("calendar-description").await, - Self::CalendarTimezone => atom("calendar-timezone").await, - Self::SupportedCalendarComponentSet => atom("supported-calendar-component-set").await, - Self::SupportedCalendarData => atom("supported-calendar-data").await, - Self::MaxResourceSize => atom("max-resource-size").await, - Self::MinDateTime => atom("min-date-time").await, - Self::MaxDateTime => atom("max-date-time").await, - Self::MaxInstances => atom("max-instances").await, - Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await, - Self::SupportedCollationSet => atom("supported-collation-set").await, - Self::CalendarData(req) => req.qwrite(xml).await, - } - } -} -impl QWrite for Property { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::CalendarDescription { lang, text } => { - let mut start = xml.create_cal_element("calendar-description"); - if let Some(the_lang) = lang { - start.push_attribute(("xml:lang", the_lang.as_str())); - } - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(text))).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::CalendarTimezone(payload) => { - let start = xml.create_cal_element("calendar-timezone"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(payload))).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::SupportedCalendarComponentSet(many_comp) => { - let start = xml.create_cal_element("supported-calendar-component-set"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for comp in many_comp.iter() { - comp.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - }, - Self::SupportedCalendarData(many_mime) => { - let start = xml.create_cal_element("supported-calendar-data"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for mime in many_mime.iter() { - mime.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - }, - Self::MaxResourceSize(bytes) => { - let start = xml.create_cal_element("max-resource-size"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(bytes.to_string().as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::MinDateTime(dt) => { - let start = xml.create_cal_element("min-date-time"); - let end = start.to_end(); - - let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT)); - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::MaxDateTime(dt) => { - let start = xml.create_cal_element("max-date-time"); - let end = start.to_end(); - - let dtstr = format!("{}", dt.format(ICAL_DATETIME_FMT)); - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(dtstr.as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::MaxInstances(count) => { - let start = xml.create_cal_element("max-instances"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(count.to_string().as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::MaxAttendeesPerInstance(count) => { - let start = xml.create_cal_element("max-attendees-per-instance"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(count.to_string().as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::SupportedCollationSet(many_collations) => { - let start = xml.create_cal_element("supported-collation-set"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for collation in many_collations.iter() { - collation.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - }, - Self::CalendarData(inner) => inner.qwrite(xml).await, - } - } -} - -// ---------------------- DAV::resourcetype ---------------------------------- -impl QWrite for ResourceType { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::Calendar => { - let empty_tag = xml.create_dav_element("calendar"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - } - } -} - -// --------------------------- DAV::error ------------------------------------ -impl QWrite for Violation { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut atom = async |c| { - let empty_tag = xml.create_cal_element(c); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }; - - match self { - //@FIXME - // DAV elements, should not be here but in RFC3744 on ACLs - // (we do not use atom as this error is in the DAV namespace, not the caldav one) - Self::NeedPrivileges => { - let empty_tag = xml.create_dav_element("need-privileges"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - - // Regular CalDAV errors - Self::ResourceMustBeNull => atom("resource-must-be-null").await, - Self::CalendarCollectionLocationOk => atom("calendar-collection-location-ok").await, - Self::ValidCalendarData => atom("valid-calendar-data").await, - Self::InitializeCalendarCollection => atom("initialize-calendar-collection").await, - Self::SupportedCalendarData => atom("supported-calendar-data").await, - Self::ValidCalendarObjectResource => atom("valid-calendar-object-resource").await, - Self::SupportedCalendarComponent => atom("supported-calendar-component").await, - Self::NoUidConflict(href) => { - let start = xml.create_cal_element("no-uid-conflict"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - href.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - }, - Self::MaxResourceSize => atom("max-resource-size").await, - Self::MinDateTime => atom("min-date-time").await, - Self::MaxDateTime => atom("max-date-time").await, - Self::MaxInstances => atom("max-instances").await, - Self::MaxAttendeesPerInstance => atom("max-attendees-per-instance").await, - Self::ValidFilter => atom("valid-filter").await, - Self::SupportedFilter { comp, prop, param } => { - let start = xml.create_cal_element("supported-filter"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for comp_item in comp.iter() { - comp_item.qwrite(xml).await?; - } - for prop_item in prop.iter() { - prop_item.qwrite(xml).await?; - } - for param_item in param.iter() { - param_item.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - }, - Self::NumberOfMatchesWithinLimits => atom("number-of-matches-within-limits").await, - } - } -} - - -// ---------------------------- Inner XML ------------------------------------ -impl QWrite for SupportedCollation { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_cal_element("supported-collation"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - - } -} - -impl QWrite for Collation { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let col = match self { - Self::AsciiCaseMap => "i;ascii-casemap", - Self::Octet => "i;octet", - Self::Unknown(v) => v.as_str(), - }; - - xml.q.write_event_async(Event::Text(BytesText::new(col))).await - } -} - -impl QWrite for CalendarDataPayload { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("calendar-data"); - if let Some(mime) = &self.mime { - start.push_attribute(("content-type", mime.content_type.as_str())); - start.push_attribute(("version", mime.version.as_str())); - } - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(self.payload.as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for CalendarDataRequest { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("calendar-data"); - if let Some(mime) = &self.mime { - start.push_attribute(("content-type", mime.content_type.as_str())); - start.push_attribute(("version", mime.version.as_str())); - } - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - if let Some(comp) = &self.comp { - comp.qwrite(xml).await?; - } - if let Some(recurrence) = &self.recurrence { - recurrence.qwrite(xml).await?; - } - if let Some(freebusy) = &self.limit_freebusy_set { - freebusy.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for CalendarDataEmpty { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut empty = xml.create_cal_element("calendar-data"); - if let Some(mime) = &self.0 { - empty.push_attribute(("content-type", mime.content_type.as_str())); - empty.push_attribute(("version", mime.version.as_str())); - } - xml.q.write_event_async(Event::Empty(empty)).await - } -} - -impl QWrite for Comp { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("comp"); - start.push_attribute(("name", self.name.as_str())); - match &self.additional_rules { - None => xml.q.write_event_async(Event::Empty(start)).await, - Some(rules) => { - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - rules.prop_kind.qwrite(xml).await?; - rules.comp_kind.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - }, - } - } -} - -impl QWrite for CompSupport { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut empty = xml.create_cal_element("comp"); - empty.push_attribute(("name", self.0.as_str())); - xml.q.write_event_async(Event::Empty(empty)).await - } -} - -impl QWrite for CompKind { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::AllComp => { - let empty_tag = xml.create_cal_element("allcomp"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - Self::Comp(many_comp) => { - for comp in many_comp.iter() { - // Required: recursion in an async fn requires boxing - // rustc --explain E0733 - Box::pin(comp.qwrite(xml)).await?; - } - Ok(()) - } - } - } -} - -impl QWrite for PropKind { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::AllProp => { - let empty_tag = xml.create_cal_element("allprop"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - Self::Prop(many_prop) => { - for prop in many_prop.iter() { - prop.qwrite(xml).await?; - } - Ok(()) - } - } - } -} - -impl QWrite for CalProp { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut empty = xml.create_cal_element("prop"); - empty.push_attribute(("name", self.name.0.as_str())); - match self.novalue { - None => (), - Some(true) => empty.push_attribute(("novalue", "yes")), - Some(false) => empty.push_attribute(("novalue", "no")), - } - xml.q.write_event_async(Event::Empty(empty)).await - } -} - -impl QWrite for RecurrenceModifier { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::Expand(exp) => exp.qwrite(xml).await, - Self::LimitRecurrenceSet(lrs) => lrs.qwrite(xml).await, - } - } -} - -impl QWrite for Expand { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut empty = xml.create_cal_element("expand"); - empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str())); - empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str())); - xml.q.write_event_async(Event::Empty(empty)).await - } -} - -impl QWrite for LimitRecurrenceSet { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut empty = xml.create_cal_element("limit-recurrence-set"); - empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str())); - empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str())); - xml.q.write_event_async(Event::Empty(empty)).await - } -} - -impl QWrite for LimitFreebusySet { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut empty = xml.create_cal_element("limit-freebusy-set"); - empty.push_attribute(("start", format!("{}", self.0.format(ICAL_DATETIME_FMT)).as_str())); - empty.push_attribute(("end", format!("{}", self.1.format(ICAL_DATETIME_FMT)).as_str())); - xml.q.write_event_async(Event::Empty(empty)).await - } -} - -impl<E: Extension> QWrite for CalendarSelector<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::AllProp => { - let empty_tag = xml.create_dav_element("allprop"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - Self::PropName => { - let empty_tag = xml.create_dav_element("propname"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - Self::Prop(prop) => prop.qwrite(xml).await, - } - } -} - -impl QWrite for CompFilter { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("comp-filter"); - start.push_attribute(("name", self.name.as_str())); - - match &self.additional_rules { - None => xml.q.write_event_async(Event::Empty(start)).await, - Some(rules) => { - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - rules.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } - } - } -} - -impl QWrite for CompFilterRules { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::IsNotDefined => { - let empty_tag = xml.create_dav_element("is-not-defined"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - Self::Matches(cfm) => cfm.qwrite(xml).await, - } - } -} - -impl QWrite for CompFilterMatch { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - if let Some(time_range) = &self.time_range { - time_range.qwrite(xml).await?; - } - - for prop_item in self.prop_filter.iter() { - prop_item.qwrite(xml).await?; - } - for comp_item in self.comp_filter.iter() { - // Required: recursion in an async fn requires boxing - // rustc --explain E0733 - Box::pin(comp_item.qwrite(xml)).await?; - } - Ok(()) - } -} - -impl QWrite for PropFilter { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("prop-filter"); - start.push_attribute(("name", self.name.as_str())); - - match &self.additional_rules { - None => xml.q.write_event_async(Event::Empty(start.clone())).await, - Some(rules) => { - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - rules.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } - } - } -} - -impl QWrite for PropFilterRules { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::IsNotDefined => { - let empty_tag = xml.create_dav_element("is-not-defined"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - Self::Match(prop_match) => prop_match.qwrite(xml).await, - } - } -} - -impl QWrite for PropFilterMatch { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - if let Some(time_range) = &self.time_range { - time_range.qwrite(xml).await?; - } - if let Some(time_or_text) = &self.time_or_text { - time_or_text.qwrite(xml).await?; - } - for param_item in self.param_filter.iter() { - param_item.qwrite(xml).await?; - } - Ok(()) - } -} - -impl QWrite for TimeOrText { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::Time(time) => time.qwrite(xml).await, - Self::Text(txt) => txt.qwrite(xml).await, - } - } -} - -impl QWrite for TextMatch { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("text-match"); - if let Some(collation) = &self.collation { - start.push_attribute(("collation", collation.as_str())); - } - match self.negate_condition { - None => (), - Some(true) => start.push_attribute(("negate-condition", "yes")), - Some(false) => start.push_attribute(("negate-condition", "no")), - } - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(self.text.as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for ParamFilter { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("param-filter"); - start.push_attribute(("name", self.name.as_str())); - - match &self.additional_rules { - None => xml.q.write_event_async(Event::Empty(start)).await, - Some(rules) => { - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - rules.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } - } - } -} - -impl QWrite for ParamFilterMatch { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::IsNotDefined => { - let empty_tag = xml.create_dav_element("is-not-defined"); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }, - Self::Match(tm) => tm.qwrite(xml).await, - } - } -} - -impl QWrite for TimeZone { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("timezone"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(self.0.as_str()))).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for Filter { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut start = xml.create_cal_element("filter"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for TimeRange { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut empty = xml.create_cal_element("time-range"); - match self { - Self::OnlyStart(start) => empty.push_attribute(("start", format!("{}", start.format(ICAL_DATETIME_FMT)).as_str())), - Self::OnlyEnd(end) => empty.push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str())), - Self::FullRange(start, end) => { - empty.push_attribute(("start", format!("{}", start.format(ICAL_DATETIME_FMT)).as_str())); - empty.push_attribute(("end", format!("{}", end.format(ICAL_DATETIME_FMT)).as_str())); - } - } - xml.q.write_event_async(Event::Empty(empty)).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::dav::types as dav; - use crate::dav::realization::Calendar; - use tokio::io::AsyncWriteExt; - use chrono::{Utc,TimeZone,DateTime}; - - async fn serialize(elem: &impl QWrite) -> String { - let mut buffer = Vec::new(); - let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); - let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4); - let ns_to_apply = vec![ - ("xmlns:D".into(), "DAV:".into()), - ("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()), - ]; - let mut writer = Writer { q, ns_to_apply }; - - elem.qwrite(&mut writer).await.expect("xml serialization"); - tokio_buffer.flush().await.expect("tokio buffer flush"); - let got = std::str::from_utf8(buffer.as_slice()).unwrap(); - - return got.into() - } - - #[tokio::test] - async fn basic_violation() { - let got = serialize( - &dav::Error::<Calendar>(vec![ - dav::Violation::Extension(Violation::ResourceMustBeNull), - ]) - ).await; - - let expected = r#"<D:error xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> - <C:resource-must-be-null/> -</D:error>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_calendar_query1_req() { - let got = serialize( - &CalendarQuery::<Calendar> { - selector: Some(CalendarSelector::Prop(dav::PropName(vec![ - dav::PropertyRequest::GetEtag, - dav::PropertyRequest::Extension(PropertyRequest::CalendarData(CalendarDataRequest { - mime: None, - comp: Some(Comp { - name: Component::VCalendar, - additional_rules: Some(CompInner { - prop_kind: PropKind::Prop(vec![ - CalProp { - name: ComponentProperty("VERSION".into()), - novalue: None, - } - ]), - comp_kind: CompKind::Comp(vec![ - Comp { - name: Component::VEvent, - additional_rules: Some(CompInner { - prop_kind: PropKind::Prop(vec![ - CalProp { name: ComponentProperty("SUMMARY".into()), novalue: None }, - CalProp { name: ComponentProperty("UID".into()), novalue: None }, - CalProp { name: ComponentProperty("DTSTART".into()), novalue: None }, - CalProp { name: ComponentProperty("DTEND".into()), novalue: None }, - CalProp { name: ComponentProperty("DURATION".into()), novalue: None }, - CalProp { name: ComponentProperty("RRULE".into()), novalue: None }, - CalProp { name: ComponentProperty("RDATE".into()), novalue: None }, - CalProp { name: ComponentProperty("EXRULE".into()), novalue: None }, - CalProp { name: ComponentProperty("EXDATE".into()), novalue: None }, - CalProp { name: ComponentProperty("RECURRENCE-ID".into()), novalue: None }, - ]), - comp_kind: CompKind::Comp(vec![]), - }), - }, - Comp { - name: Component::VTimeZone, - additional_rules: None, - } - ]), - }), - }), - recurrence: None, - limit_freebusy_set: None, - })), - ]))), - filter: Filter(CompFilter { - name: Component::VCalendar, - additional_rules: Some(CompFilterRules::Matches(CompFilterMatch { - time_range: None, - prop_filter: vec![], - comp_filter: vec![ - CompFilter { - name: Component::VEvent, - additional_rules: Some(CompFilterRules::Matches(CompFilterMatch { - time_range: Some(TimeRange::FullRange( - Utc.with_ymd_and_hms(2006,1,4,0,0,0).unwrap(), - Utc.with_ymd_and_hms(2006,1,5,0,0,0).unwrap(), - )), - prop_filter: vec![], - comp_filter: vec![], - })), - }, - ], - })), - }), - timezone: None, - } - ).await; - - let expected = r#"<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> - <D:prop> - <D:getetag/> - <C:calendar-data> - <C:comp name="VCALENDAR"> - <C:prop name="VERSION"/> - <C:comp name="VEVENT"> - <C:prop name="SUMMARY"/> - <C:prop name="UID"/> - <C:prop name="DTSTART"/> - <C:prop name="DTEND"/> - <C:prop name="DURATION"/> - <C:prop name="RRULE"/> - <C:prop name="RDATE"/> - <C:prop name="EXRULE"/> - <C:prop name="EXDATE"/> - <C:prop name="RECURRENCE-ID"/> - </C:comp> - <C:comp name="VTIMEZONE"/> - </C:comp> - </C:calendar-data> - </D:prop> - <C:filter> - <C:comp-filter name="VCALENDAR"> - <C:comp-filter name="VEVENT"> - <C:time-range start="20060104T000000Z" end="20060105T000000Z"/> - </C:comp-filter> - </C:comp-filter> - </C:filter> -</C:calendar-query>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_calendar_query1_res() { - let got = serialize( - &dav::Multistatus::<Calendar,dav::PropValue<Calendar>> { - responses: vec![ - dav::Response { - status_or_propstat: dav::StatusOrPropstat::PropStat( - dav::Href("http://cal.example.com/bernard/work/abcd2.ics".into()), - vec![dav::PropStat { - prop: dav::PropValue(vec![ - dav::Property::GetEtag("\"fffff-abcd2\"".into()), - dav::Property::Extension(Property::CalendarData(CalendarDataPayload { - mime: None, - payload: "PLACEHOLDER".into() - })), - ]), - status: dav::Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }] - ), - location: None, - error: None, - responsedescription: None, - }, - dav::Response { - status_or_propstat: dav::StatusOrPropstat::PropStat( - dav::Href("http://cal.example.com/bernard/work/abcd3.ics".into()), - vec![dav::PropStat { - prop: dav::PropValue(vec![ - dav::Property::GetEtag("\"fffff-abcd3\"".into()), - dav::Property::Extension(Property::CalendarData(CalendarDataPayload{ - mime: None, - payload: "PLACEHOLDER".into(), - })), - ]), - status: dav::Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }] - ), - location: None, - error: None, - responsedescription: None, - }, - ], - responsedescription: None, - }, - ).await; - - let expected = r#"<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> - <D:response> - <D:href>http://cal.example.com/bernard/work/abcd2.ics</D:href> - <D:propstat> - <D:prop> - <D:getetag>"fffff-abcd2"</D:getetag> - <C:calendar-data>PLACEHOLDER</C:calendar-data> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> - <D:response> - <D:href>http://cal.example.com/bernard/work/abcd3.ics</D:href> - <D:propstat> - <D:prop> - <D:getetag>"fffff-abcd3"</D:getetag> - <C:calendar-data>PLACEHOLDER</C:calendar-data> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> -</D:multistatus>"#; - - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } -} diff --git a/src/dav/caltypes.rs b/src/dav/caltypes.rs deleted file mode 100644 index befecef..0000000 --- a/src/dav/caltypes.rs +++ /dev/null @@ -1,1440 +0,0 @@ -#![allow(dead_code)] - -use chrono::{DateTime,Utc}; -use super::types as dav; -use super::xml; - -//@FIXME ACL (rfc3744) is missing, required -//@FIXME Versioning (rfc3253) is missing, required -//@FIXME WebDAV sync (rfc6578) is missing, optional -// For reference, SabreDAV guide gives high-level & real-world overview: -// https://sabre.io/dav/building-a-caldav-client/ -// For reference, non-official extensions documented by SabreDAV: -// https://github.com/apple/ccs-calendarserver/tree/master/doc/Extensions - - -// ----- Root elements ----- - -// --- (MKCALENDAR PART) --- - -/// If a request body is included, it MUST be a CALDAV:mkcalendar XML -/// element. Instruction processing MUST occur in the order -/// instructions are received (i.e., from top to bottom). -/// Instructions MUST either all be executed or none executed. Thus, -/// if any error occurs during processing, all executed instructions -/// MUST be undone and a proper error result returned. Instruction -/// processing details can be found in the definition of the DAV:set -/// instruction in Section 12.13.2 of [RFC2518]. -/// -/// <!ELEMENT mkcalendar (DAV:set)> -#[derive(Debug, PartialEq)] -pub struct MkCalendar<E: dav::Extension>(pub dav::Set<E>); - - -/// If a response body for a successful request is included, it MUST -/// be a CALDAV:mkcalendar-response XML element. -/// -/// <!ELEMENT mkcalendar-response ANY> -/// -/// ---- -/// -/// ANY is not satisfying, so looking at RFC5689 -/// https://www.rfc-editor.org/rfc/rfc5689.html#section-5.2 -/// -/// Definition: -/// -/// <!ELEMENT mkcol-response (propstat+)> -#[derive(Debug, PartialEq)] -pub struct MkCalendarResponse<E: dav::Extension, N: xml::Node<N>>(pub Vec<dav::PropStat<E,N>>); - -// --- (REPORT PART) --- - -/// Name: calendar-query -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Defines a report for querying calendar object resources. -/// -/// Description: See Section 7.8. -/// -/// Definition: -/// -/// <!ELEMENT calendar-query ((DAV:allprop | -/// DAV:propname | -/// DAV:prop)?, filter, timezone?)> -#[derive(Debug, PartialEq)] -pub struct CalendarQuery<E: dav::Extension> { - pub selector: Option<CalendarSelector<E>>, - pub filter: Filter, - pub timezone: Option<TimeZone>, -} - -/// Name: calendar-multiget -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: CalDAV report used to retrieve specific calendar object -/// resources. -/// -/// Description: See Section 7.9. -/// -/// Definition: -/// -/// <!ELEMENT calendar-multiget ((DAV:allprop | -/// DAV:propname | -/// DAV:prop)?, DAV:href+)> -#[derive(Debug, PartialEq)] -pub struct CalendarMultiget<E: dav::Extension> { - pub selector: Option<CalendarSelector<E>>, - pub href: Vec<dav::Href>, -} - -/// Name: free-busy-query -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: CalDAV report used to generate a VFREEBUSY to determine -/// busy time over a specific time range. -/// -/// Description: See Section 7.10. -/// -/// Definition: -/// <!ELEMENT free-busy-query (time-range)> -#[derive(Debug, PartialEq)] -pub struct FreeBusyQuery(pub TimeRange); - -// ----- Hooks ----- -#[derive(Debug, PartialEq)] -pub enum ResourceType { - Calendar, -} - -/// Check the matching Property object for documentation -#[derive(Debug, PartialEq)] -pub enum PropertyRequest { - CalendarDescription, - CalendarTimezone, - SupportedCalendarComponentSet, - SupportedCalendarData, - MaxResourceSize, - MinDateTime, - MaxDateTime, - MaxInstances, - MaxAttendeesPerInstance, - SupportedCollationSet, - CalendarData(CalendarDataRequest), -} - -#[derive(Debug, PartialEq)] -pub enum Property { - /// Name: calendar-description - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Provides a human-readable description of the calendar - /// collection. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MAY be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). An xml:lang attribute indicating the human - /// language of the description SHOULD be set for this property by - /// clients or through server provisioning. Servers MUST return any - /// xml:lang attribute if set for the property. - /// - /// Description: If present, the property contains a description of the - /// calendar collection that is suitable for presentation to a user. - /// If not present, the client should assume no description for the - /// calendar collection. - /// - /// Definition: - /// - /// <!ELEMENT calendar-description (#PCDATA)> - /// PCDATA value: string - /// - /// Example: - /// - /// <C:calendar-description xml:lang="fr-CA" - /// xmlns:C="urn:ietf:params:xml:ns:caldav" - /// >Calendrier de Mathilde Desruisseaux</C:calendar-description> - CalendarDescription { - lang: Option<String>, - text: String, - }, - - /// 5.2.2. CALDAV:calendar-timezone Property - /// - /// Name: calendar-timezone - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Specifies a time zone on a calendar collection. - /// - /// Conformance: This property SHOULD be defined on all calendar - /// collections. If defined, it SHOULD NOT be returned by a PROPFIND - /// DAV:allprop request (as defined in Section 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:calendar-timezone property is used to - /// specify the time zone the server should rely on to resolve "date" - /// values and "date with local time" values (i.e., floating time) to - /// "date with UTC time" values. The server will require this - /// information to determine if a calendar component scheduled with - /// "date" values or "date with local time" values overlaps a CALDAV: - /// time-range specified in a CALDAV:calendar-query REPORT. The - /// server will also require this information to compute the proper - /// FREEBUSY time period as "date with UTC time" in the VFREEBUSY - /// component returned in a response to a CALDAV:free-busy-query - /// REPORT request that takes into account calendar components - /// scheduled with "date" values or "date with local time" values. In - /// the absence of this property, the server MAY rely on the time zone - /// of their choice. - /// - /// Note: The iCalendar data embedded within the CALDAV:calendar- - /// timezone XML element MUST follow the standard XML character data - /// encoding rules, including use of <, >, & etc. entity - /// encoding or the use of a <![CDATA[ ... ]]> construct. In the - /// later case, the iCalendar data cannot contain the character - /// sequence "]]>", which is the end delimiter for the CDATA section. - /// - /// Definition: - /// - /// <!ELEMENT calendar-timezone (#PCDATA)> - /// PCDATA value: an iCalendar object with exactly one VTIMEZONE component. - /// - /// Example: - /// - /// <C:calendar-timezone - /// xmlns:C="urn:ietf:params:xml:ns:caldav">BEGIN:VCALENDAR - /// PRODID:-//Example Corp.//CalDAV Client//EN - /// VERSION:2.0 - /// BEGIN:VTIMEZONE - /// TZID:US-Eastern - /// LAST-MODIFIED:19870101T000000Z - /// BEGIN:STANDARD - /// DTSTART:19671029T020000 - /// RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - /// TZOFFSETFROM:-0400 - /// TZOFFSETTO:-0500 - /// TZNAME:Eastern Standard Time (US & Canada) - /// END:STANDARD - /// BEGIN:DAYLIGHT - /// DTSTART:19870405T020000 - /// RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 - /// TZOFFSETFROM:-0500 - /// TZOFFSETTO:-0400 - /// TZNAME:Eastern Daylight Time (US & Canada) - /// END:DAYLIGHT - /// END:VTIMEZONE - /// END:VCALENDAR - /// </C:calendar-timezone> - //@FIXME we might want to put a buffer here or an iCal parsed object - CalendarTimezone(String), - - /// Name: supported-calendar-component-set - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Specifies the calendar component types (e.g., VEVENT, - /// VTODO, etc.) that calendar object resources can contain in the - /// calendar collection. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MUST be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:supported-calendar-component-set property is - /// used to specify restrictions on the calendar component types that - /// calendar object resources may contain in a calendar collection. - /// Any attempt by the client to store calendar object resources with - /// component types not listed in this property, if it exists, MUST - /// result in an error, with the CALDAV:supported-calendar-component - /// precondition (Section 5.3.2.1) being violated. Since this - /// property is protected, it cannot be changed by clients using a - /// PROPPATCH request. However, clients can initialize the value of - /// this property when creating a new calendar collection with - /// MKCALENDAR. The empty-element tag <C:comp name="VTIMEZONE"/> MUST - /// only be specified if support for calendar object resources that - /// only contain VTIMEZONE components is provided or desired. Support - /// for VTIMEZONE components in calendar object resources that contain - /// VEVENT or VTODO components is always assumed. In the absence of - /// this property, the server MUST accept all component types, and the - /// client can assume that all component types are accepted. - /// - /// Definition: - /// - /// <!ELEMENT supported-calendar-component-set (comp+)> - /// - /// Example: - /// - /// <C:supported-calendar-component-set - /// xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// <C:comp name="VEVENT"/> - /// <C:comp name="VTODO"/> - /// </C:supported-calendar-component-set> - SupportedCalendarComponentSet(Vec<CompSupport>), - - /// Name: supported-calendar-data - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Specifies what media types are allowed for calendar object - /// resources in a calendar collection. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MUST be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:supported-calendar-data property is used to - /// specify the media type supported for the calendar object resources - /// contained in a given calendar collection (e.g., iCalendar version - /// 2.0). Any attempt by the client to store calendar object - /// resources with a media type not listed in this property MUST - /// result in an error, with the CALDAV:supported-calendar-data - /// precondition (Section 5.3.2.1) being violated. In the absence of - /// this property, the server MUST only accept data with the media - /// type "text/calendar" and iCalendar version 2.0, and clients can - /// assume that the server will only accept this data. - /// - /// Definition: - /// - /// <!ELEMENT supported-calendar-data (calendar-data+)> - /// - /// Example: - /// - /// <C:supported-calendar-data - /// xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// <C:calendar-data content-type="text/calendar" version="2.0"/> - /// </C:supported-calendar-data> - /// - /// ----- - /// - /// <!ELEMENT calendar-data EMPTY> - /// - /// when nested in the CALDAV:supported-calendar-data property - /// to specify a supported media type for calendar object - /// resources; - SupportedCalendarData(Vec<CalendarDataEmpty>), - - /// Name: max-resource-size - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Provides a numeric value indicating the maximum size of a - /// resource in octets that the server is willing to accept when a - /// calendar object resource is stored in a calendar collection. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MUST be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:max-resource-size is used to specify a - /// numeric value that represents the maximum size in octets that the - /// server is willing to accept when a calendar object resource is - /// stored in a calendar collection. Any attempt to store a calendar - /// object resource exceeding this size MUST result in an error, with - /// the CALDAV:max-resource-size precondition (Section 5.3.2.1) being - /// violated. In the absence of this property, the client can assume - /// that the server will allow storing a resource of any reasonable - /// size. - /// - /// Definition: - /// - /// <!ELEMENT max-resource-size (#PCDATA)> - /// PCDATA value: a numeric value (positive integer) - /// - /// Example: - /// - /// <C:max-resource-size xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// 102400 - /// </C:max-resource-size> - MaxResourceSize(u64), - - /// CALDAV:min-date-time Property - /// - /// Name: min-date-time - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Provides a DATE-TIME value indicating the earliest date and - /// time (in UTC) that the server is willing to accept for any DATE or - /// DATE-TIME value in a calendar object resource stored in a calendar - /// collection. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MUST be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:min-date-time is used to specify an - /// iCalendar DATE-TIME value in UTC that indicates the earliest - /// inclusive date that the server is willing to accept for any - /// explicit DATE or DATE-TIME value in a calendar object resource - /// stored in a calendar collection. Any attempt to store a calendar - /// object resource using a DATE or DATE-TIME value earlier than this - /// value MUST result in an error, with the CALDAV:min-date-time - /// precondition (Section 5.3.2.1) being violated. Note that servers - /// MUST accept recurring components that specify instances beyond - /// this limit, provided none of those instances have been overridden. - /// In that case, the server MAY simply ignore those instances outside - /// of the acceptable range when processing reports on the calendar - /// object resource. In the absence of this property, the client can - /// assume any valid iCalendar date may be used at least up to the - /// CALDAV:max-date-time value, if that is defined. - /// - /// Definition: - /// - /// <!ELEMENT min-date-time (#PCDATA)> - /// PCDATA value: an iCalendar format DATE-TIME value in UTC - /// - /// Example: - /// - /// <C:min-date-time xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// 19000101T000000Z - /// </C:min-date-time> - MinDateTime(DateTime<Utc>), - - /// CALDAV:max-date-time Property - /// - /// Name: max-date-time - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Provides a DATE-TIME value indicating the latest date and - /// time (in UTC) that the server is willing to accept for any DATE or - /// DATE-TIME value in a calendar object resource stored in a calendar - /// collection. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MUST be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:max-date-time is used to specify an - /// iCalendar DATE-TIME value in UTC that indicates the inclusive - /// latest date that the server is willing to accept for any date or - /// time value in a calendar object resource stored in a calendar - /// collection. Any attempt to store a calendar object resource using - /// a DATE or DATE-TIME value later than this value MUST result in an - /// error, with the CALDAV:max-date-time precondition - /// (Section 5.3.2.1) being violated. Note that servers MUST accept - /// recurring components that specify instances beyond this limit, - /// provided none of those instances have been overridden. In that - /// case, the server MAY simply ignore those instances outside of the - /// acceptable range when processing reports on the calendar object - /// resource. In the absence of this property, the client can assume - /// any valid iCalendar date may be used at least down to the CALDAV: - /// min-date-time value, if that is defined. - /// - /// Definition: - /// - /// <!ELEMENT max-date-time (#PCDATA)> - /// PCDATA value: an iCalendar format DATE-TIME value in UTC - /// - /// Example: - /// - /// <C:max-date-time xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// 20491231T235959Z - /// </C:max-date-time> - MaxDateTime(DateTime<Utc>), - - /// CALDAV:max-instances Property - /// - /// Name: max-instances - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Provides a numeric value indicating the maximum number of - /// recurrence instances that a calendar object resource stored in a - /// calendar collection can generate. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MUST be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:max-instances is used to specify a numeric - /// value that indicates the maximum number of recurrence instances - /// that a calendar object resource stored in a calendar collection - /// can generate. Any attempt to store a calendar object resource - /// with a recurrence pattern that generates more instances than this - /// value MUST result in an error, with the CALDAV:max-instances - /// precondition (Section 5.3.2.1) being violated. In the absence of - /// this property, the client can assume that the server has no limits - /// on the number of recurrence instances it can handle or expand. - /// - /// Definition: - /// - /// <!ELEMENT max-instances (#PCDATA)> - /// PCDATA value: a numeric value (integer greater than zero) - /// - /// Example: - /// - /// <C:max-instances xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// 100 - /// </C:max-instances> - MaxInstances(u64), - - /// CALDAV:max-attendees-per-instance Property - /// - /// Name: max-attendees-per-instance - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Provides a numeric value indicating the maximum number of - /// ATTENDEE properties in any instance of a calendar object resource - /// stored in a calendar collection. - /// - /// Conformance: This property MAY be defined on any calendar - /// collection. If defined, it MUST be protected and SHOULD NOT be - /// returned by a PROPFIND DAV:allprop request (as defined in Section - /// 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:max-attendees-per-instance is used to - /// specify a numeric value that indicates the maximum number of - /// iCalendar ATTENDEE properties on any one instance of a calendar - /// object resource stored in a calendar collection. Any attempt to - /// store a calendar object resource with more ATTENDEE properties per - /// instance than this value MUST result in an error, with the CALDAV: - /// max-attendees-per-instance precondition (Section 5.3.2.1) being - /// violated. In the absence of this property, the client can assume - /// that the server can handle any number of ATTENDEE properties in a - /// calendar component. - /// - /// Definition: - /// - /// <!ELEMENT max-attendees-per-instance (#PCDATA)> - /// PCDATA value: a numeric value (integer greater than zero) - /// - /// Example: - /// - /// <C:max-attendees-per-instance - /// xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// 25 - /// </C:max-attendees-per-instance> - MaxAttendeesPerInstance(u64), - - /// Name: supported-collation-set - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Identifies the set of collations supported by the server - /// for text matching operations. - /// - /// Conformance: This property MUST be defined on any resource that - /// supports a report that does text matching. If defined, it MUST be - /// protected and SHOULD NOT be returned by a PROPFIND DAV:allprop - /// request (as defined in Section 12.14.1 of [RFC2518]). - /// - /// Description: The CALDAV:supported-collation-set property contains - /// zero or more CALDAV:supported-collation elements, which specify - /// the collection identifiers of the collations supported by the - /// server. - /// - /// Definition: - /// - /// <!ELEMENT supported-collation-set (supported-collation*)> - /// <!ELEMENT supported-collation (#PCDATA)> - /// - /// Example: - /// - /// <C:supported-collation-set - /// xmlns:C="urn:ietf:params:xml:ns:caldav"> - /// <C:supported-collation>i;ascii-casemap</C:supported-collation> - /// <C:supported-collation>i;octet</C:supported-collation> - /// </C:supported-collation-set> - SupportedCollationSet(Vec<SupportedCollation>), - - /// Name: calendar-data - /// - /// Namespace: urn:ietf:params:xml:ns:caldav - /// - /// Purpose: Specified one of the following: - /// - /// 1. A supported media type for calendar object resources when - /// nested in the CALDAV:supported-calendar-data property; - /// - /// 2. The parts of a calendar object resource should be returned by - /// a calendaring report; - /// - /// 3. The content of a calendar object resource in a response to a - /// calendaring report. - /// - /// Description: When nested in the CALDAV:supported-calendar-data - /// property, the CALDAV:calendar-data XML element specifies a media - /// type supported by the CalDAV server for calendar object resources. - /// - /// When used in a calendaring REPORT request, the CALDAV:calendar- - /// data XML element specifies which parts of calendar object - /// resources need to be returned in the response. If the CALDAV: - /// calendar-data XML element doesn't contain any CALDAV:comp element, - /// calendar object resources will be returned in their entirety. - /// - /// Finally, when used in a calendaring REPORT response, the CALDAV: - /// calendar-data XML element specifies the content of a calendar - /// object resource. Given that XML parsers normalize the two- - /// character sequence CRLF (US-ASCII decimal 13 and US-ASCII decimal - /// 10) to a single LF character (US-ASCII decimal 10), the CR - /// character (US-ASCII decimal 13) MAY be omitted in calendar object - /// resources specified in the CALDAV:calendar-data XML element. - /// Furthermore, calendar object resources specified in the CALDAV: - /// calendar-data XML element MAY be invalid per their media type - /// specification if the CALDAV:calendar-data XML element part of the - /// calendaring REPORT request did not specify required properties - /// (e.g., UID, DTSTAMP, etc.), or specified a CALDAV:prop XML element - /// with the "novalue" attribute set to "yes". - /// - /// Note: The CALDAV:calendar-data XML element is specified in requests - /// and responses inside the DAV:prop XML element as if it were a - /// WebDAV property. However, the CALDAV:calendar-data XML element is - /// not a WebDAV property and, as such, is not returned in PROPFIND - /// responses, nor used in PROPPATCH requests. - /// - /// Note: The iCalendar data embedded within the CALDAV:calendar-data - /// XML element MUST follow the standard XML character data encoding - /// rules, including use of <, >, & etc. entity encoding or - /// the use of a <![CDATA[ ... ]]> construct. In the later case, the - /// iCalendar data cannot contain the character sequence "]]>", which - /// is the end delimiter for the CDATA section. - CalendarData(CalendarDataPayload), -} - -#[derive(Debug, PartialEq)] -pub enum Violation { - /// (DAV:resource-must-be-null): A resource MUST NOT exist at the - /// Request-URI; - ResourceMustBeNull, - - /// (CALDAV:calendar-collection-location-ok): The Request-URI MUST - /// identify a location where a calendar collection can be created; - CalendarCollectionLocationOk, - - /// (CALDAV:valid-calendar-data): The time zone specified in CALDAV: - /// calendar-timezone property MUST be a valid iCalendar object - /// containing a single valid VTIMEZONE component. - ValidCalendarData, - - ///@FIXME should not be here but in RFC3744 - /// !!! ERRATA 1002 !!! - /// (DAV:need-privileges): The DAV:bind privilege MUST be granted to - /// the current user on the parent collection of the Request-URI. - NeedPrivileges, - - /// (CALDAV:initialize-calendar-collection): A new calendar collection - /// exists at the Request-URI. The DAV:resourcetype of the calendar - /// collection MUST contain both DAV:collection and CALDAV:calendar - /// XML elements. - InitializeCalendarCollection, - - /// (CALDAV:supported-calendar-data): The resource submitted in the - /// PUT request, or targeted by a COPY or MOVE request, MUST be a - /// supported media type (i.e., iCalendar) for calendar object - /// resources; - SupportedCalendarData, - - /// (CALDAV:valid-calendar-object-resource): The resource submitted in - /// the PUT request, or targeted by a COPY or MOVE request, MUST obey - /// all restrictions specified in Section 4.1 (e.g., calendar object - /// resources MUST NOT contain more than one type of calendar - /// component, calendar object resources MUST NOT specify the - /// iCalendar METHOD property, etc.); - ValidCalendarObjectResource, - - /// (CALDAV:supported-calendar-component): The resource submitted in - /// the PUT request, or targeted by a COPY or MOVE request, MUST - /// contain a type of calendar component that is supported in the - /// targeted calendar collection; - SupportedCalendarComponent, - - /// (CALDAV:no-uid-conflict): The resource submitted in the PUT - /// request, or targeted by a COPY or MOVE request, MUST NOT specify - /// an iCalendar UID property value already in use in the targeted - /// calendar collection or overwrite an existing calendar object - /// resource with one that has a different UID property value. - /// Servers SHOULD report the URL of the resource that is already - /// making use of the same UID property value in the DAV:href element; - /// - /// <!ELEMENT no-uid-conflict (DAV:href)> - NoUidConflict(dav::Href), - - /// (CALDAV:max-resource-size): The resource submitted in the PUT - /// request, or targeted by a COPY or MOVE request, MUST have an octet - /// size less than or equal to the value of the CALDAV:max-resource- - /// size property value (Section 5.2.5) on the calendar collection - /// where the resource will be stored; - MaxResourceSize, - - /// (CALDAV:min-date-time): The resource submitted in the PUT request, - /// or targeted by a COPY or MOVE request, MUST have all of its - /// iCalendar DATE or DATE-TIME property values (for each recurring - /// instance) greater than or equal to the value of the CALDAV:min- - /// date-time property value (Section 5.2.6) on the calendar - /// collection where the resource will be stored; - MinDateTime, - - /// (CALDAV:max-date-time): The resource submitted in the PUT request, - /// or targeted by a COPY or MOVE request, MUST have all of its - /// iCalendar DATE or DATE-TIME property values (for each recurring - /// instance) less than the value of the CALDAV:max-date-time property - /// value (Section 5.2.7) on the calendar collection where the - /// resource will be stored; - MaxDateTime, - - /// (CALDAV:max-instances): The resource submitted in the PUT request, - /// or targeted by a COPY or MOVE request, MUST generate a number of - /// recurring instances less than or equal to the value of the CALDAV: - /// max-instances property value (Section 5.2.8) on the calendar - /// collection where the resource will be stored; - MaxInstances, - - /// (CALDAV:max-attendees-per-instance): The resource submitted in the - /// PUT request, or targeted by a COPY or MOVE request, MUST have a - /// number of ATTENDEE properties on any one instance less than or - /// equal to the value of the CALDAV:max-attendees-per-instance - /// property value (Section 5.2.9) on the calendar collection where - /// the resource will be stored; - MaxAttendeesPerInstance, - - /// (CALDAV:valid-filter): The CALDAV:filter XML element (see - /// Section 9.7) specified in the REPORT request MUST be valid. For - /// instance, a CALDAV:filter cannot nest a <C:comp name="VEVENT"> - /// element in a <C:comp name="VTODO"> element, and a CALDAV:filter - /// cannot nest a <C:time-range start="..." end="..."> element in a - /// <C:prop name="SUMMARY"> element. - ValidFilter, - - /// (CALDAV:supported-filter): The CALDAV:comp-filter (see - /// Section 9.7.1), CALDAV:prop-filter (see Section 9.7.2), and - /// CALDAV:param-filter (see Section 9.7.3) XML elements used in the - /// CALDAV:filter XML element (see Section 9.7) in the REPORT request - /// only make reference to components, properties, and parameters for - /// which queries are supported by the server, i.e., if the CALDAV: - /// filter element attempts to reference an unsupported component, - /// property, or parameter, this precondition is violated. Servers - /// SHOULD report the CALDAV:comp-filter, CALDAV:prop-filter, or - /// CALDAV:param-filter for which it does not provide support. - /// - /// <!ELEMENT supported-filter (comp-filter*, - /// prop-filter*, - /// param-filter*)> - SupportedFilter { - comp: Vec<CompFilter>, - prop: Vec<PropFilter>, - param: Vec<ParamFilter>, - }, - - /// (DAV:number-of-matches-within-limits): The number of matching - /// calendar object resources must fall within server-specific, - /// predefined limits. For example, this condition might be triggered - /// if a search specification would cause the return of an extremely - /// large number of responses. - NumberOfMatchesWithinLimits, -} - -// -------- Inner XML elements --------- - -/// Some of the reports defined in this section do text matches of -/// character strings provided by the client and are compared to stored -/// calendar data. Since iCalendar data is, by default, encoded in the -/// UTF-8 charset and may include characters outside the US-ASCII charset -/// range in some property and parameter values, there is a need to -/// ensure that text matching follows well-defined rules. -/// -/// To deal with this, this specification makes use of the IANA Collation -/// Registry defined in [RFC4790] to specify collations that may be used -/// to carry out the text comparison operations with a well-defined rule. -/// -/// The comparisons used in CalDAV are all "substring" matches, as per -/// [RFC4790], Section 4.2. Collations supported by the server MUST -/// support "substring" match operations. -/// -/// CalDAV servers are REQUIRED to support the "i;ascii-casemap" and -/// "i;octet" collations, as described in [RFC4790], and MAY support -/// other collations. -/// -/// Servers MUST advertise the set of collations that they support via -/// the CALDAV:supported-collation-set property defined on any resource -/// that supports reports that use collations. -/// -/// Clients MUST only use collations from the list advertised by the -/// server. -/// -/// In the absence of a collation explicitly specified by the client, or -/// if the client specifies the "default" collation identifier (as -/// defined in [RFC4790], Section 3.1), the server MUST default to using -/// "i;ascii-casemap" as the collation. -/// -/// Wildcards (as defined in [RFC4790], Section 3.2) MUST NOT be used in -/// the collation identifier. -/// -/// If the client chooses a collation not supported by the server, the -/// server MUST respond with a CALDAV:supported-collation precondition -/// error response. -#[derive(Debug, PartialEq)] -pub struct SupportedCollation(pub Collation); - -/// <!ELEMENT calendar-data (#PCDATA)> -/// PCDATA value: iCalendar object -/// -/// when nested in the DAV:prop XML element in a calendaring -/// REPORT response to specify the content of a returned -/// calendar object resource. -#[derive(Debug, PartialEq)] -pub struct CalendarDataPayload { - pub mime: Option<CalendarDataSupport>, - pub payload: String, -} - -/// <!ELEMENT calendar-data (comp?, -/// (expand | limit-recurrence-set)?, -/// limit-freebusy-set?)> -/// -/// when nested in the DAV:prop XML element in a calendaring -/// REPORT request to specify which parts of calendar object -/// resources should be returned in the response; -#[derive(Debug, PartialEq)] -pub struct CalendarDataRequest { - pub mime: Option<CalendarDataSupport>, - pub comp: Option<Comp>, - pub recurrence: Option<RecurrenceModifier>, - pub limit_freebusy_set: Option<LimitFreebusySet>, -} - -/// calendar-data specialization for Property -/// -/// <!ELEMENT calendar-data EMPTY> -/// -/// when nested in the CALDAV:supported-calendar-data property -/// to specify a supported media type for calendar object -/// resources; -#[derive(Debug, PartialEq)] -pub struct CalendarDataEmpty(pub Option<CalendarDataSupport>); - -/// <!ATTLIST calendar-data content-type CDATA "text/calendar" -/// version CDATA "2.0"> -/// content-type value: a MIME media type -/// version value: a version string -/// attributes can be used on all three variants of the -/// CALDAV:calendar-data XML element. -#[derive(Debug, PartialEq)] -pub struct CalendarDataSupport { - pub content_type: String, - pub version: String, -} - -/// Name: comp -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Defines which component types to return. -/// -/// Description: The name value is a calendar component name (e.g., -/// VEVENT). -/// -/// Definition: -/// -/// <!ELEMENT comp ((allprop | prop*), (allcomp | comp*))> -/// <!ATTLIST comp name CDATA #REQUIRED> -/// name value: a calendar component name -/// -/// Note: The CALDAV:prop and CALDAV:allprop elements have the same name -/// as the DAV:prop and DAV:allprop elements defined in [RFC2518]. -/// However, the CALDAV:prop and CALDAV:allprop elements are defined -/// in the "urn:ietf:params:xml:ns:caldav" namespace instead of the -/// "DAV:" namespace. -#[derive(Debug, PartialEq)] -pub struct Comp { - pub name: Component, - pub additional_rules: Option<CompInner>, -} - -#[derive(Debug, PartialEq)] -pub struct CompInner { - pub prop_kind: PropKind, - pub comp_kind: CompKind, -} - -/// For SupportedCalendarComponentSet -/// -/// Definition: -/// -/// <!ELEMENT supported-calendar-component-set (comp+)> -/// -/// Example: -/// -/// <C:supported-calendar-component-set -/// xmlns:C="urn:ietf:params:xml:ns:caldav"> -/// <C:comp name="VEVENT"/> -/// <C:comp name="VTODO"/> -/// </C:supported-calendar-component-set> -#[derive(Debug, PartialEq)] -pub struct CompSupport(pub Component); - -/// Name: allcomp -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies that all components shall be returned. -/// -/// Description: The CALDAV:allcomp XML element can be used when the -/// client wants all types of components returned by a calendaring -/// REPORT request. -/// -/// Definition: -/// -/// <!ELEMENT allcomp EMPTY> -#[derive(Debug, PartialEq)] -pub enum CompKind { - AllComp, - Comp(Vec<Comp>), -} - -/// Name: allprop -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies that all properties shall be returned. -/// -/// Description: The CALDAV:allprop XML element can be used when the -/// client wants all properties of components returned by a -/// calendaring REPORT request. -/// -/// Definition: -/// -/// <!ELEMENT allprop EMPTY> -/// -/// Note: The CALDAV:allprop element has the same name as the DAV: -/// allprop element defined in [RFC2518]. However, the CALDAV:allprop -/// element is defined in the "urn:ietf:params:xml:ns:caldav" -/// namespace instead of the "DAV:" namespace. -#[derive(Debug, PartialEq)] -pub enum PropKind { - AllProp, - Prop(Vec<CalProp>), -} - -/// Name: prop -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Defines which properties to return in the response. -/// -/// Description: The "name" attribute specifies the name of the calendar -/// property to return (e.g., ATTENDEE). The "novalue" attribute can -/// be used by clients to request that the actual value of the -/// property not be returned (if the "novalue" attribute is set to -/// "yes"). In that case, the server will return just the iCalendar -/// property name and any iCalendar parameters and a trailing ":" -/// without the subsequent value data. -/// -/// Definition: -/// <!ELEMENT prop EMPTY> -/// <!ATTLIST prop name CDATA #REQUIRED novalue (yes | no) "no"> -/// name value: a calendar property name -/// novalue value: "yes" or "no" -/// -/// Note: The CALDAV:prop element has the same name as the DAV:prop -/// element defined in [RFC2518]. However, the CALDAV:prop element is -/// defined in the "urn:ietf:params:xml:ns:caldav" namespace instead -/// of the "DAV:" namespace. -#[derive(Debug, PartialEq)] -pub struct CalProp { - pub name: ComponentProperty, - pub novalue: Option<bool>, -} - -#[derive(Debug, PartialEq)] -pub enum RecurrenceModifier { - Expand(Expand), - LimitRecurrenceSet(LimitRecurrenceSet), -} - -/// Name: expand -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Forces the server to expand recurring components into -/// individual recurrence instances. -/// -/// Description: The CALDAV:expand XML element specifies that for a -/// given calendaring REPORT request, the server MUST expand the -/// recurrence set into calendar components that define exactly one -/// recurrence instance, and MUST return only those whose scheduled -/// time intersect a specified time range. -/// -/// The "start" attribute specifies the inclusive start of the time -/// range, and the "end" attribute specifies the non-inclusive end of -/// the time range. Both attributes are specified as date with UTC -/// time value. The value of the "end" attribute MUST be greater than -/// the value of the "start" attribute. -/// -/// The server MUST use the same logic as defined for CALDAV:time- -/// range to determine if a recurrence instance intersects the -/// specified time range. -/// -/// Recurring components, other than the initial instance, MUST -/// include a RECURRENCE-ID property indicating which instance they -/// refer to. -/// -/// The returned calendar components MUST NOT use recurrence -/// properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT -/// have reference to or include VTIMEZONE components. Date and local -/// time with reference to time zone information MUST be converted -/// into date with UTC time. -/// -/// Definition: -/// -/// <!ELEMENT expand EMPTY> -/// <!ATTLIST expand start CDATA #REQUIRED -/// end CDATA #REQUIRED> -/// start value: an iCalendar "date with UTC time" -/// end value: an iCalendar "date with UTC time" -#[derive(Debug, PartialEq)] -pub struct Expand(pub DateTime<Utc>, pub DateTime<Utc>); - -/// CALDAV:limit-recurrence-set XML Element -/// -/// Name: limit-recurrence-set -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies a time range to limit the set of "overridden -/// components" returned by the server. -/// -/// Description: The CALDAV:limit-recurrence-set XML element specifies -/// that for a given calendaring REPORT request, the server MUST -/// return, in addition to the "master component", only the -/// "overridden components" that impact a specified time range. An -/// overridden component impacts a time range if its current start and -/// end times overlap the time range, or if the original start and end -/// times -- the ones that would have been used if the instance were -/// not overridden -- overlap the time range. -/// -/// The "start" attribute specifies the inclusive start of the time -/// range, and the "end" attribute specifies the non-inclusive end of -/// the time range. Both attributes are specified as date with UTC -/// time value. The value of the "end" attribute MUST be greater than -/// the value of the "start" attribute. -/// -/// The server MUST use the same logic as defined for CALDAV:time- -/// range to determine if the current or original scheduled time of an -/// "overridden" recurrence instance intersects the specified time -/// range. -/// -/// Overridden components that have a RANGE parameter on their -/// RECURRENCE-ID property may specify one or more instances in the -/// recurrence set, and some of those instances may fall within the -/// specified time range or may have originally fallen within the -/// specified time range prior to being overridden. If that is the -/// case, the overridden component MUST be included in the results, as -/// it has a direct impact on the interpretation of instances within -/// the specified time range. -/// -/// Definition: -/// -/// <!ELEMENT limit-recurrence-set EMPTY> -/// <!ATTLIST limit-recurrence-set start CDATA #REQUIRED -/// end CDATA #REQUIRED> -/// start value: an iCalendar "date with UTC time" -/// end value: an iCalendar "date with UTC time" -#[derive(Debug, PartialEq)] -pub struct LimitRecurrenceSet(pub DateTime<Utc>, pub DateTime<Utc>); - -/// Name: limit-freebusy-set -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies a time range to limit the set of FREEBUSY values -/// returned by the server. -/// -/// Description: The CALDAV:limit-freebusy-set XML element specifies -/// that for a given calendaring REPORT request, the server MUST only -/// return the FREEBUSY property values of a VFREEBUSY component that -/// intersects a specified time range. -/// -/// The "start" attribute specifies the inclusive start of the time -/// range, and the "end" attribute specifies the non-inclusive end of -/// the time range. Both attributes are specified as "date with UTC -/// time" value. The value of the "end" attribute MUST be greater -/// than the value of the "start" attribute. -/// -/// The server MUST use the same logic as defined for CALDAV:time- -/// range to determine if a FREEBUSY property value intersects the -/// specified time range. -/// -/// Definition: -/// <!ELEMENT limit-freebusy-set EMPTY> -/// <!ATTLIST limit-freebusy-set start CDATA #REQUIRED -/// end CDATA #REQUIRED> -/// start value: an iCalendar "date with UTC time" -/// end value: an iCalendar "date with UTC time" -#[derive(Debug, PartialEq)] -pub struct LimitFreebusySet(pub DateTime<Utc>, pub DateTime<Utc>); - -/// Used by CalendarQuery & CalendarMultiget -#[derive(Debug, PartialEq)] -pub enum CalendarSelector<E: dav::Extension> { - AllProp, - PropName, - Prop(dav::PropName<E>), -} - -/// Name: comp-filter -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies search criteria on calendar components. -/// -/// Description: The CALDAV:comp-filter XML element specifies a query -/// targeted at the calendar object (i.e., VCALENDAR) or at a specific -/// calendar component type (e.g., VEVENT). The scope of the -/// CALDAV:comp-filter XML element is the calendar object when used as -/// a child of the CALDAV:filter XML element. The scope of the -/// CALDAV:comp-filter XML element is the enclosing calendar component -/// when used as a child of another CALDAV:comp-filter XML element. A -/// CALDAV:comp-filter is said to match if: -/// -/// * The CALDAV:comp-filter XML element is empty and the calendar -/// object or calendar component type specified by the "name" -/// attribute exists in the current scope; -/// -/// or: -/// -/// * The CALDAV:comp-filter XML element contains a CALDAV:is-not- -/// defined XML element and the calendar object or calendar -/// component type specified by the "name" attribute does not exist -/// in the current scope; -/// -/// or: -/// -/// * The CALDAV:comp-filter XML element contains a CALDAV:time-range -/// XML element and at least one recurrence instance in the -/// targeted calendar component is scheduled to overlap the -/// specified time range, and all specified CALDAV:prop-filter and -/// CALDAV:comp-filter child XML elements also match the targeted -/// calendar component; -/// -/// or: -/// -/// * The CALDAV:comp-filter XML element only contains CALDAV:prop- -/// filter and CALDAV:comp-filter child XML elements that all match -/// the targeted calendar component. -/// -/// Definition: -/// <!ELEMENT comp-filter (is-not-defined | (time-range?, -/// prop-filter*, comp-filter*))> -/// -/// <!ATTLIST comp-filter name CDATA #REQUIRED> -/// name value: a calendar object or calendar component -/// type (e.g., VEVENT) -#[derive(Debug, PartialEq)] -pub struct CompFilter { - pub name: Component, - // Option 1 = None, Option 2, 3, 4 = Some - pub additional_rules: Option<CompFilterRules>, -} -#[derive(Debug, PartialEq)] -pub enum CompFilterRules { - // Option 2 - IsNotDefined, - // Options 3 & 4 - Matches(CompFilterMatch), -} -#[derive(Debug, PartialEq)] -pub struct CompFilterMatch { - pub time_range: Option<TimeRange>, - pub prop_filter: Vec<PropFilter>, - pub comp_filter: Vec<CompFilter>, -} - -/// Name: prop-filter -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies search criteria on calendar properties. -/// -/// Description: The CALDAV:prop-filter XML element specifies a query -/// targeted at a specific calendar property (e.g., CATEGORIES) in the -/// scope of the enclosing calendar component. A calendar property is -/// said to match a CALDAV:prop-filter if: -/// -/// * The CALDAV:prop-filter XML element is empty and a property of -/// the type specified by the "name" attribute exists in the -/// enclosing calendar component; -/// -/// or: -/// -/// * The CALDAV:prop-filter XML element contains a CALDAV:is-not- -/// defined XML element and no property of the type specified by -/// the "name" attribute exists in the enclosing calendar -/// component; -/// -/// or: -/// -/// * The CALDAV:prop-filter XML element contains a CALDAV:time-range -/// XML element and the property value overlaps the specified time -/// range, and all specified CALDAV:param-filter child XML elements -/// also match the targeted property; -/// -/// or: -/// -/// * The CALDAV:prop-filter XML element contains a CALDAV:text-match -/// XML element and the property value matches it, and all -/// specified CALDAV:param-filter child XML elements also match the -/// targeted property; -/// -/// Definition: -/// -/// <!ELEMENT prop-filter (is-not-defined | -/// ((time-range | text-match)?, -/// param-filter*))> -/// -/// <!ATTLIST prop-filter name CDATA #REQUIRED> -/// name value: a calendar property name (e.g., ATTENDEE) -#[derive(Debug, PartialEq)] -pub struct PropFilter { - pub name: Component, - // None = Option 1, Some() = Option 2, 3 & 4 - pub additional_rules: Option<PropFilterRules>, -} -#[derive(Debug, PartialEq)] -pub enum PropFilterRules { - // Option 2 - IsNotDefined, - // Options 3 & 4 - Match(PropFilterMatch), -} -#[derive(Debug, PartialEq)] -pub struct PropFilterMatch { - pub time_range: Option<TimeRange>, - pub time_or_text: Option<TimeOrText>, - pub param_filter: Vec<ParamFilter>, -} -#[derive(Debug, PartialEq)] -pub enum TimeOrText { - Time(TimeRange), - Text(TextMatch), -} - -/// Name: text-match -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies a substring match on a property or parameter -/// value. -/// -/// Description: The CALDAV:text-match XML element specifies text used -/// for a substring match against the property or parameter value -/// specified in a calendaring REPORT request. -/// -/// The "collation" attribute is used to select the collation that the -/// server MUST use for character string matching. In the absence of -/// this attribute, the server MUST use the "i;ascii-casemap" -/// collation. -/// -/// The "negate-condition" attribute is used to indicate that this -/// test returns a match if the text matches when the attribute value -/// is set to "no", or return a match if the text does not match, if -/// the attribute value is set to "yes". For example, this can be -/// used to match components with a STATUS property not set to -/// CANCELLED. -/// -/// Definition: -/// <!ELEMENT text-match (#PCDATA)> -/// PCDATA value: string -/// <!ATTLIST text-match collation CDATA "i;ascii-casemap" -/// negate-condition (yes | no) "no"> -#[derive(Debug, PartialEq)] -pub struct TextMatch { - pub collation: Option<Collation>, - pub negate_condition: Option<bool>, - pub text: String, -} - -/// Name: param-filter -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Limits the search to specific parameter values. -/// -/// Description: The CALDAV:param-filter XML element specifies a query -/// targeted at a specific calendar property parameter (e.g., -/// PARTSTAT) in the scope of the calendar property on which it is -/// defined. A calendar property parameter is said to match a CALDAV: -/// param-filter if: -/// -/// * The CALDAV:param-filter XML element is empty and a parameter of -/// the type specified by the "name" attribute exists on the -/// calendar property being examined; -/// -/// or: -/// -/// * The CALDAV:param-filter XML element contains a CALDAV:is-not- -/// defined XML element and no parameter of the type specified by -/// the "name" attribute exists on the calendar property being -/// examined; -/// -/// Definition: -/// -/// <!ELEMENT param-filter (is-not-defined | text-match?)> -/// -/// <!ATTLIST param-filter name CDATA #REQUIRED> -/// name value: a property parameter name (e.g., PARTSTAT) -#[derive(Debug, PartialEq)] -pub struct ParamFilter { - pub name: PropertyParameter, - pub additional_rules: Option<ParamFilterMatch>, -} -#[derive(Debug, PartialEq)] -pub enum ParamFilterMatch { - IsNotDefined, - Match(TextMatch), -} - -/// CALDAV:is-not-defined XML Element -/// -/// Name: is-not-defined -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies that a match should occur if the enclosing -/// component, property, or parameter does not exist. -/// -/// Description: The CALDAV:is-not-defined XML element specifies that a -/// match occurs if the enclosing component, property, or parameter -/// value specified in a calendaring REPORT request does not exist in -/// the calendar data being tested. -/// -/// Definition: -/// <!ELEMENT is-not-defined EMPTY> -/* CURRENTLY INLINED */ - - - -/// Name: timezone -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies the time zone component to use when determining -/// the results of a report. -/// -/// Description: The CALDAV:timezone XML element specifies that for a -/// given calendaring REPORT request, the server MUST rely on the -/// specified VTIMEZONE component instead of the CALDAV:calendar- -/// timezone property of the calendar collection, in which the -/// calendar object resource is contained to resolve "date" values and -/// "date with local time" values (i.e., floating time) to "date with -/// UTC time" values. The server will require this information to -/// determine if a calendar component scheduled with "date" values or -/// "date with local time" values intersects a CALDAV:time-range -/// specified in a CALDAV:calendar-query REPORT. -/// -/// Note: The iCalendar data embedded within the CALDAV:timezone XML -/// element MUST follow the standard XML character data encoding -/// rules, including use of <, >, & etc. entity encoding or -/// the use of a <![CDATA[ ... ]]> construct. In the later case, the -/// -/// iCalendar data cannot contain the character sequence "]]>", which -/// is the end delimiter for the CDATA section. -/// -/// Definition: -/// -/// <!ELEMENT timezone (#PCDATA)> -/// PCDATA value: an iCalendar object with exactly one VTIMEZONE -#[derive(Debug, PartialEq)] -pub struct TimeZone(pub String); - -/// Name: filter -/// -/// Namespace: urn:ietf:params:xml:ns:caldav -/// -/// Purpose: Specifies a filter to limit the set of calendar components -/// returned by the server. -/// -/// Description: The CALDAV:filter XML element specifies the search -/// filter used to limit the calendar components returned by a -/// calendaring REPORT request. -/// -/// Definition: -/// <!ELEMENT filter (comp-filter)> -#[derive(Debug, PartialEq)] -pub struct Filter(pub CompFilter); - -/// Name: time-range -/// -/// Definition: -/// -/// <!ELEMENT time-range EMPTY> -/// <!ATTLIST time-range start CDATA #IMPLIED -/// end CDATA #IMPLIED> -/// start value: an iCalendar "date with UTC time" -/// end value: an iCalendar "date with UTC time" -#[derive(Debug, PartialEq)] -pub enum TimeRange { - OnlyStart(DateTime<Utc>), - OnlyEnd(DateTime<Utc>), - FullRange(DateTime<Utc>, DateTime<Utc>), -} - -// ----------------------- ENUM ATTRIBUTES --------------------- - -/// Known components -#[derive(Debug, PartialEq)] -pub enum Component { - VCalendar, - VJournal, - VFreeBusy, - VEvent, - VTodo, - VAlarm, - VTimeZone, - Unknown(String), -} -impl Component { - pub fn as_str<'a>(&'a self) -> &'a str { - match self { - Self::VCalendar => "VCALENDAR", - Self::VJournal => "VJOURNAL", - Self::VFreeBusy => "VFREEBUSY", - Self::VEvent => "VEVENT", - Self::VTodo => "VTODO", - Self::VAlarm => "VALARM", - Self::VTimeZone => "VTIMEZONE", - Self::Unknown(c) => c, - } - } -} - -/// name="VERSION", name="SUMMARY", etc. -/// Can be set on different objects: VCalendar, VEvent, etc. -/// Might be replaced by an enum later -#[derive(Debug, PartialEq)] -pub struct ComponentProperty(pub String); - -/// like PARSTAT -#[derive(Debug, PartialEq)] -pub struct PropertyParameter(pub String); -impl PropertyParameter { - pub fn as_str<'a>(&'a self) -> &'a str { - self.0.as_str() - } -} - -#[derive(Default,Debug,PartialEq)] -pub enum Collation { - #[default] - AsciiCaseMap, - Octet, - Unknown(String), -} -impl Collation { - pub fn as_str<'a>(&'a self) -> &'a str { - match self { - Self::AsciiCaseMap => "i;ascii-casemap", - Self::Octet => "i;octet", - Self::Unknown(c) => c.as_str(), - } - } -} diff --git a/src/dav/decoder.rs b/src/dav/decoder.rs deleted file mode 100644 index aa3c7e5..0000000 --- a/src/dav/decoder.rs +++ /dev/null @@ -1,948 +0,0 @@ -use std::borrow::Cow; -use std::future::Future; - -use quick_xml::events::{Event, BytesStart, BytesDecl, BytesText}; -use quick_xml::events::attributes::AttrError; -use quick_xml::name::{Namespace, QName, PrefixDeclaration, ResolveResult, ResolveResult::*}; -use quick_xml::reader::NsReader; -use tokio::io::AsyncBufRead; - -use super::types::*; -use super::error::ParsingError; -use super::xml::{Node, QRead, Reader, IRead, DAV_URN, CAL_URN}; - -//@TODO (1) Rewrite all objects as Href, -// where we return Ok(None) instead of trying to find the object at any cost. -// Add a xml.find<E: Qread>() -> Result<Option<E>, ParsingError> or similar for the cases we -// really need the object -// (2) Rewrite QRead and replace Result<Option<_>, _> with Result<_, _>, not found being a possible -// error. -// (3) Rewrite vectors with xml.collect<E: QRead>() -> Result<Vec<E>, _> -// (4) Something for alternatives would be great but no idea yet - -// ---- ROOT ---- - -/// Propfind request -impl<E: Extension> QRead<PropFind<E>> for PropFind<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "propfind").await?; - let propfind: PropFind<E> = loop { - // allprop - if let Some(_) = xml.maybe_open(DAV_URN, "allprop").await? { - let includ = xml.maybe_find::<Include<E>>().await?; - xml.close().await?; - break PropFind::AllProp(includ) - } - - // propname - if let Some(_) = xml.maybe_open(DAV_URN, "propname").await? { - xml.close().await?; - break PropFind::PropName - } - - // prop - let (mut maybe_prop, mut dirty) = (None, false); - xml.maybe_read::<PropName<E>>(&mut maybe_prop, &mut dirty).await?; - if let Some(prop) = maybe_prop { - break PropFind::Prop(prop) - } - - // not found, skipping - xml.skip().await?; - }; - xml.close().await?; - - Ok(propfind) - } -} - -/// PROPPATCH request -impl<E: Extension> QRead<PropertyUpdate<E>> for PropertyUpdate<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "propertyupdate").await?; - let collected_items = xml.collect::<PropertyUpdateItem<E>>().await?; - xml.close().await?; - Ok(PropertyUpdate(collected_items)) - } -} - -/// Generic response -impl<E: Extension, N: Node<N>> QRead<Multistatus<E,N>> for Multistatus<E,N> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "multistatus").await?; - let mut responses = Vec::new(); - let mut responsedescription = None; - - loop { - let mut dirty = false; - xml.maybe_push(&mut responses, &mut dirty).await?; - xml.maybe_read(&mut responsedescription, &mut dirty).await?; - if !dirty { - match xml.peek() { - Event::End(_) => break, - _ => xml.skip().await?, - }; - } - } - - xml.close().await?; - Ok(Multistatus { responses, responsedescription }) - } -} - -// LOCK REQUEST -impl QRead<LockInfo> for LockInfo { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "lockinfo").await?; - let (mut m_scope, mut m_type, mut owner) = (None, None, None); - loop { - let mut dirty = false; - xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty).await?; - xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?; - xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?; - - if !dirty { - match xml.peek() { - Event::End(_) => break, - _ => xml.skip().await?, - }; - } - } - xml.close().await?; - match (m_scope, m_type) { - (Some(lockscope), Some(locktype)) => Ok(LockInfo { lockscope, locktype, owner }), - _ => Err(ParsingError::MissingChild), - } - } -} - -// LOCK RESPONSE -impl<E: Extension> QRead<PropValue<E>> for PropValue<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "prop").await?; - let mut acc = xml.collect::<Property<E>>().await?; - xml.close().await?; - Ok(PropValue(acc)) - } -} - - -/// Error response -impl<E: Extension> QRead<Error<E>> for Error<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "error").await?; - let violations = xml.collect::<Violation<E>>().await?; - xml.close().await?; - Ok(Error(violations)) - } -} - - - -// ---- INNER XML -impl<E: Extension, N: Node<N>> QRead<Response<E,N>> for Response<E,N> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "response").await?; - let (mut status, mut error, mut responsedescription, mut location) = (None, None, None, None); - let mut href = Vec::new(); - let mut propstat = Vec::new(); - - loop { - let mut dirty = false; - xml.maybe_read::<Status>(&mut status, &mut dirty).await?; - xml.maybe_push::<Href>(&mut href, &mut dirty).await?; - xml.maybe_push::<PropStat<E,N>>(&mut propstat, &mut dirty).await?; - xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?; - xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty).await?; - xml.maybe_read::<Location>(&mut location, &mut dirty).await?; - - if !dirty { - match xml.peek() { - Event::End(_) => break, - _ => { xml.skip().await? }, - }; - } - } - - xml.close().await?; - match (status, &propstat[..], &href[..]) { - (Some(status), &[], &[_, ..]) => Ok(Response { - status_or_propstat: StatusOrPropstat::Status(href, status), - error, responsedescription, location, - }), - (None, &[_, ..], &[_, ..]) => Ok(Response { - status_or_propstat: StatusOrPropstat::PropStat(href.into_iter().next().unwrap(), propstat), - error, responsedescription, location, - }), - (Some(_), &[_, ..], _) => Err(ParsingError::InvalidValue), - _ => Err(ParsingError::MissingChild), - } - } -} - -impl<E: Extension, N: Node<N>> QRead<PropStat<E,N>> for PropStat<E,N> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "propstat").await?; - - let (mut m_prop, mut m_status, mut error, mut responsedescription) = (None, None, None, None); - - loop { - let mut dirty = false; - xml.maybe_read::<N>(&mut m_prop, &mut dirty).await?; - xml.maybe_read::<Status>(&mut m_status, &mut dirty).await?; - xml.maybe_read::<Error<E>>(&mut error, &mut dirty).await?; - xml.maybe_read::<ResponseDescription>(&mut responsedescription, &mut dirty).await?; - - if !dirty { - match xml.peek() { - Event::End(_) => break, - _ => xml.skip().await?, - }; - } - } - - xml.close().await?; - match (m_prop, m_status) { - (Some(prop), Some(status)) => Ok(PropStat { prop, status, error, responsedescription }), - _ => Err(ParsingError::MissingChild), - } - } -} - -impl QRead<Status> for Status { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "status").await?; - let fullcode = xml.tag_string().await?; - let txtcode = fullcode.splitn(3, ' ').nth(1).ok_or(ParsingError::InvalidValue)?; - let code = http::status::StatusCode::from_bytes(txtcode.as_bytes()).or(Err(ParsingError::InvalidValue))?; - xml.close().await?; - Ok(Status(code)) - } -} - -impl QRead<ResponseDescription> for ResponseDescription { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "responsedescription").await?; - let cnt = xml.tag_string().await?; - xml.close().await?; - Ok(ResponseDescription(cnt)) - } -} - -impl QRead<Location> for Location { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "location").await?; - let href = xml.find::<Href>().await?; - xml.close().await?; - Ok(Location(href)) - } -} - -impl<E: Extension> QRead<PropertyUpdateItem<E>> for PropertyUpdateItem<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - match Remove::qread(xml).await { - Err(ParsingError::Recoverable) => (), - otherwise => return otherwise.map(PropertyUpdateItem::Remove), - } - Set::qread(xml).await.map(PropertyUpdateItem::Set) - } -} - -impl<E: Extension> QRead<Remove<E>> for Remove<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "remove").await?; - let propname = xml.find::<PropName<E>>().await?; - xml.close().await?; - Ok(Remove(propname)) - } -} - -impl<E: Extension> QRead<Set<E>> for Set<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "set").await?; - let propvalue = xml.find::<PropValue<E>>().await?; - xml.close().await?; - Ok(Set(propvalue)) - } -} - -impl<E: Extension> QRead<Violation<E>> for Violation<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - if xml.maybe_open(DAV_URN, "lock-token-matches-request-uri").await?.is_some() { - xml.close().await?; - Ok(Violation::LockTokenMatchesRequestUri) - } else if xml.maybe_open(DAV_URN, "lock-token-submitted").await?.is_some() { - let links = xml.collect::<Href>().await?; - xml.close().await?; - Ok(Violation::LockTokenSubmitted(links)) - } else if xml.maybe_open(DAV_URN, "no-conflicting-lock").await?.is_some() { - let links = xml.collect::<Href>().await?; - xml.close().await?; - Ok(Violation::NoConflictingLock(links)) - } else if xml.maybe_open(DAV_URN, "no-external-entities").await?.is_some() { - xml.close().await?; - Ok(Violation::NoExternalEntities) - } else if xml.maybe_open(DAV_URN, "preserved-live-properties").await?.is_some() { - xml.close().await?; - Ok(Violation::PreservedLiveProperties) - } else if xml.maybe_open(DAV_URN, "propfind-finite-depth").await?.is_some() { - xml.close().await?; - Ok(Violation::PropfindFiniteDepth) - } else if xml.maybe_open(DAV_URN, "cannot-modify-protected-property").await?.is_some() { - xml.close().await?; - Ok(Violation::CannotModifyProtectedProperty) - } else { - E::Error::qread(xml).await.map(Violation::Extension) - } - } -} - -impl<E: Extension> QRead<Include<E>> for Include<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "include").await?; - let acc = xml.collect::<PropertyRequest<E>>().await?; - xml.close().await?; - Ok(Include(acc)) - } -} - -impl<E: Extension> QRead<PropName<E>> for PropName<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "prop").await?; - let acc = xml.collect::<PropertyRequest<E>>().await?; - xml.close().await?; - Ok(PropName(acc)) - } -} - -impl<E: Extension> QRead<PropertyRequest<E>> for PropertyRequest<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - let maybe = if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() { - Some(PropertyRequest::CreationDate) - } else if xml.maybe_open(DAV_URN, "displayname").await?.is_some() { - Some(PropertyRequest::DisplayName) - } else if xml.maybe_open(DAV_URN, "getcontentlanguage").await?.is_some() { - Some(PropertyRequest::GetContentLanguage) - } else if xml.maybe_open(DAV_URN, "getcontentlength").await?.is_some() { - Some(PropertyRequest::GetContentLength) - } else if xml.maybe_open(DAV_URN, "getcontenttype").await?.is_some() { - Some(PropertyRequest::GetContentType) - } else if xml.maybe_open(DAV_URN, "getetag").await?.is_some() { - Some(PropertyRequest::GetEtag) - } else if xml.maybe_open(DAV_URN, "getlastmodified").await?.is_some() { - Some(PropertyRequest::GetLastModified) - } else if xml.maybe_open(DAV_URN, "lockdiscovery").await?.is_some() { - Some(PropertyRequest::LockDiscovery) - } else if xml.maybe_open(DAV_URN, "resourcetype").await?.is_some() { - Some(PropertyRequest::ResourceType) - } else if xml.maybe_open(DAV_URN, "supportedlock").await?.is_some() { - Some(PropertyRequest::SupportedLock) - } else { - None - }; - - match maybe { - Some(pr) => { - xml.close().await?; - Ok(pr) - }, - None => E::PropertyRequest::qread(xml).await.map(PropertyRequest::Extension), - } - } -} - -impl<E: Extension> QRead<Property<E>> for Property<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - use chrono::{DateTime, FixedOffset, TimeZone}; - - // Core WebDAV properties - if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() { - let datestr = xml.tag_string().await?; - xml.close().await?; - return Ok(Property::CreationDate(DateTime::parse_from_rfc3339(datestr.as_str())?)) - } else if xml.maybe_open(DAV_URN, "displayname").await?.is_some() { - let name = xml.tag_string().await?; - xml.close().await?; - return Ok(Property::DisplayName(name)) - } else if xml.maybe_open(DAV_URN, "getcontentlanguage").await?.is_some() { - let lang = xml.tag_string().await?; - xml.close().await?; - return Ok(Property::GetContentLanguage(lang)) - } else if xml.maybe_open(DAV_URN, "getcontentlength").await?.is_some() { - let cl = xml.tag_string().await?.parse::<u64>()?; - xml.close().await?; - return Ok(Property::GetContentLength(cl)) - } else if xml.maybe_open(DAV_URN, "getcontenttype").await?.is_some() { - let ct = xml.tag_string().await?; - xml.close().await?; - return Ok(Property::GetContentType(ct)) - } else if xml.maybe_open(DAV_URN, "getetag").await?.is_some() { - let etag = xml.tag_string().await?; - xml.close().await?; - return Ok(Property::GetEtag(etag)) - } else if xml.maybe_open(DAV_URN, "getlastmodified").await?.is_some() { - let datestr = xml.tag_string().await?; - xml.close().await?; - return Ok(Property::GetLastModified(DateTime::parse_from_rfc2822(datestr.as_str())?)) - } else if xml.maybe_open(DAV_URN, "lockdiscovery").await?.is_some() { - let acc = xml.collect::<ActiveLock>().await?; - xml.close().await?; - return Ok(Property::LockDiscovery(acc)) - } else if xml.maybe_open(DAV_URN, "resourcetype").await?.is_some() { - let acc = xml.collect::<ResourceType<E>>().await?; - xml.close().await?; - return Ok(Property::ResourceType(acc)) - } else if xml.maybe_open(DAV_URN, "supportedlock").await?.is_some() { - let acc = xml.collect::<LockEntry>().await?; - xml.close().await?; - return Ok(Property::SupportedLock(acc)) - } - - // Option 2: an extension property, delegating - E::Property::qread(xml).await.map(Property::Extension) - } -} - -impl QRead<ActiveLock> for ActiveLock { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "activelock").await?; - let (mut m_scope, mut m_type, mut m_depth, mut owner, mut timeout, mut locktoken, mut m_root) = - (None, None, None, None, None, None, None); - - loop { - let mut dirty = false; - xml.maybe_read::<LockScope>(&mut m_scope, &mut dirty).await?; - xml.maybe_read::<LockType>(&mut m_type, &mut dirty).await?; - xml.maybe_read::<Depth>(&mut m_depth, &mut dirty).await?; - xml.maybe_read::<Owner>(&mut owner, &mut dirty).await?; - xml.maybe_read::<Timeout>(&mut timeout, &mut dirty).await?; - xml.maybe_read::<LockToken>(&mut locktoken, &mut dirty).await?; - xml.maybe_read::<LockRoot>(&mut m_root, &mut dirty).await?; - - if !dirty { - match xml.peek() { - Event::End(_) => break, - _ => { xml.skip().await?; }, - } - } - } - - xml.close().await?; - match (m_scope, m_type, m_depth, m_root) { - (Some(lockscope), Some(locktype), Some(depth), Some(lockroot)) => - Ok(ActiveLock { lockscope, locktype, depth, owner, timeout, locktoken, lockroot }), - _ => Err(ParsingError::MissingChild), - } - } -} - -impl QRead<Depth> for Depth { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "depth").await?; - let depth_str = xml.tag_string().await?; - xml.close().await?; - match depth_str.as_str() { - "0" => Ok(Depth::Zero), - "1" => Ok(Depth::One), - "infinity" => Ok(Depth::Infinity), - _ => Err(ParsingError::WrongToken), - } - } -} - -impl QRead<Owner> for Owner { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "owner").await?; - - let mut owner = Owner::Unknown; - loop { - match xml.peek() { - Event::Text(_) | Event::CData(_) => { - let txt = xml.tag_string().await?; - if matches!(owner, Owner::Unknown) { - owner = Owner::Txt(txt); - } - } - Event::Start(_) | Event::Empty(_) => { - match Href::qread(xml).await { - Ok(href) => { owner = Owner::Href(href); }, - Err(ParsingError::Recoverable) => { xml.skip().await?; }, - Err(e) => return Err(e), - } - } - Event::End(_) => break, - _ => { xml.skip().await?; }, - } - }; - xml.close().await?; - Ok(owner) - } -} - -impl QRead<Timeout> for Timeout { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - const SEC_PFX: &str = "SEC_PFX"; - xml.open(DAV_URN, "timeout").await?; - - let timeout = match xml.tag_string().await?.as_str() { - "Infinite" => Timeout::Infinite, - seconds => match seconds.strip_prefix(SEC_PFX) { - Some(secs) => Timeout::Seconds(secs.parse::<u32>()?), - None => return Err(ParsingError::InvalidValue), - }, - }; - - xml.close().await?; - Ok(timeout) - } -} - -impl QRead<LockToken> for LockToken { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "locktoken").await?; - let href = Href::qread(xml).await?; - xml.close().await?; - Ok(LockToken(href)) - } -} - -impl QRead<LockRoot> for LockRoot { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "lockroot").await?; - let href = Href::qread(xml).await?; - xml.close().await?; - Ok(LockRoot(href)) - } -} - -impl<E: Extension> QRead<ResourceType<E>> for ResourceType<E> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - if xml.maybe_open(DAV_URN, "collection").await?.is_some() { - xml.close().await?; - return Ok(ResourceType::Collection) - } - - E::ResourceType::qread(xml).await.map(ResourceType::Extension) - } -} - -impl QRead<LockEntry> for LockEntry { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "lockentry").await?; - let (mut maybe_scope, mut maybe_type) = (None, None); - - loop { - let mut dirty = false; - xml.maybe_read::<LockScope>(&mut maybe_scope, &mut dirty).await?; - xml.maybe_read::<LockType>(&mut maybe_type, &mut dirty).await?; - if !dirty { - match xml.peek() { - Event::End(_) => break, - _ => xml.skip().await?, - }; - } - } - - xml.close().await?; - match (maybe_scope, maybe_type) { - (Some(lockscope), Some(locktype)) => Ok(LockEntry { lockscope, locktype }), - _ => Err(ParsingError::MissingChild), - } - } -} - -impl QRead<LockScope> for LockScope { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "lockscope").await?; - - let lockscope = loop { - if xml.maybe_open(DAV_URN, "exclusive").await?.is_some() { - xml.close().await?; - break LockScope::Exclusive - } else if xml.maybe_open(DAV_URN, "shared").await?.is_some() { - xml.close().await?; - break LockScope::Shared - } - - xml.skip().await?; - }; - - xml.close().await?; - Ok(lockscope) - } -} - -impl QRead<LockType> for LockType { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "locktype").await?; - - let locktype = loop { - if xml.maybe_open(DAV_URN, "write").await?.is_some() { - xml.close().await?; - break LockType::Write - } - - xml.skip().await?; - }; - - xml.close().await?; - Ok(locktype) - } -} - -impl QRead<Href> for Href { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> { - xml.open(DAV_URN, "href").await?; - let mut url = xml.tag_string().await?; - xml.close().await?; - Ok(Href(url)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::{FixedOffset, DateTime, TimeZone, Utc}; - use crate::dav::realization::Core; - - #[tokio::test] - async fn basic_propfind_propname() { - let src = r#"<?xml version="1.0" encoding="utf-8" ?> -<rando/> -<garbage><old/></garbage> -<D:propfind xmlns:D="DAV:"> - <D:propname/> -</D:propfind> -"#; - - let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); - let got = rdr.find::<PropFind::<Core>>().await.unwrap(); - - assert_eq!(got, PropFind::<Core>::PropName); - } - - #[tokio::test] - async fn basic_propfind_prop() { - let src = r#"<?xml version="1.0" encoding="utf-8" ?> -<rando/> -<garbage><old/></garbage> -<D:propfind xmlns:D="DAV:"> - <D:prop> - <D:displayname/> - <D:getcontentlength/> - <D:getcontenttype/> - <D:getetag/> - <D:getlastmodified/> - <D:resourcetype/> - <D:supportedlock/> - </D:prop> -</D:propfind> -"#; - - let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); - let got = rdr.find::<PropFind::<Core>>().await.unwrap(); - - assert_eq!(got, PropFind::Prop(PropName(vec![ - PropertyRequest::DisplayName, - PropertyRequest::GetContentLength, - PropertyRequest::GetContentType, - PropertyRequest::GetEtag, - PropertyRequest::GetLastModified, - PropertyRequest::ResourceType, - PropertyRequest::SupportedLock, - ]))); - } - - #[tokio::test] - async fn rfc_lock_error() { - let src = r#"<?xml version="1.0" encoding="utf-8" ?> - <D:error xmlns:D="DAV:"> - <D:lock-token-submitted> - <D:href>/locked/</D:href> - </D:lock-token-submitted> - </D:error>"#; - - let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); - let got = rdr.find::<Error::<Core>>().await.unwrap(); - - assert_eq!(got, Error(vec![ - Violation::LockTokenSubmitted(vec![ - Href("/locked/".into()) - ]) - ])); - } - - - #[tokio::test] - async fn rfc_propertyupdate() { - let src = r#"<?xml version="1.0" encoding="utf-8" ?> - <D:propertyupdate xmlns:D="DAV:" - xmlns:Z="http://ns.example.com/standards/z39.50/"> - <D:set> - <D:prop> - <Z:Authors> - <Z:Author>Jim Whitehead</Z:Author> - <Z:Author>Roy Fielding</Z:Author> - </Z:Authors> - </D:prop> - </D:set> - <D:remove> - <D:prop><Z:Copyright-Owner/></D:prop> - </D:remove> - </D:propertyupdate>"#; - - let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); - let got = rdr.find::<PropertyUpdate::<Core>>().await.unwrap(); - - assert_eq!(got, PropertyUpdate(vec![ - PropertyUpdateItem::Set(Set(PropValue(vec![]))), - PropertyUpdateItem::Remove(Remove(PropName(vec![]))), - ])); - } - - #[tokio::test] - async fn rfc_lockinfo() { - let src = r#" -<?xml version="1.0" encoding="utf-8" ?> -<D:lockinfo xmlns:D='DAV:'> - <D:lockscope><D:exclusive/></D:lockscope> - <D:locktype><D:write/></D:locktype> - <D:owner> - <D:href>http://example.org/~ejw/contact.html</D:href> - </D:owner> -</D:lockinfo> -"#; - - let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); - let got = rdr.find::<LockInfo>().await.unwrap(); - - assert_eq!(got, LockInfo { - lockscope: LockScope::Exclusive, - locktype: LockType::Write, - owner: Some(Owner::Href(Href("http://example.org/~ejw/contact.html".into()))), - }); - } - - #[tokio::test] - async fn rfc_multistatus_name() { - let src = r#" -<?xml version="1.0" encoding="utf-8" ?> - <multistatus xmlns="DAV:"> - <response> - <href>http://www.example.com/container/</href> - <propstat> - <prop xmlns:R="http://ns.example.com/boxschema/"> - <R:bigbox/> - <R:author/> - <creationdate/> - <displayname/> - <resourcetype/> - <supportedlock/> - </prop> - <status>HTTP/1.1 200 OK</status> - </propstat> - </response> - <response> - <href>http://www.example.com/container/front.html</href> - <propstat> - <prop xmlns:R="http://ns.example.com/boxschema/"> - <R:bigbox/> - <creationdate/> - <displayname/> - <getcontentlength/> - <getcontenttype/> - <getetag/> - <getlastmodified/> - <resourcetype/> - <supportedlock/> - </prop> - <status>HTTP/1.1 200 OK</status> - </propstat> - </response> - </multistatus> -"#; - - let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); - let got = rdr.find::<Multistatus::<Core, PropName<Core>>>().await.unwrap(); - - assert_eq!(got, Multistatus { - responses: vec![ - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("http://www.example.com/container/".into()), - vec![PropStat { - prop: PropName(vec![ - PropertyRequest::CreationDate, - PropertyRequest::DisplayName, - PropertyRequest::ResourceType, - PropertyRequest::SupportedLock, - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }], - ), - error: None, - responsedescription: None, - location: None, - }, - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("http://www.example.com/container/front.html".into()), - vec![PropStat { - prop: PropName(vec![ - PropertyRequest::CreationDate, - PropertyRequest::DisplayName, - PropertyRequest::GetContentLength, - PropertyRequest::GetContentType, - PropertyRequest::GetEtag, - PropertyRequest::GetLastModified, - PropertyRequest::ResourceType, - PropertyRequest::SupportedLock, - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }], - ), - error: None, - responsedescription: None, - location: None, - }, - ], - responsedescription: None, - }); - } - - - #[tokio::test] - async fn rfc_multistatus_value() { - let src = r#" - <?xml version="1.0" encoding="utf-8" ?> - <D:multistatus xmlns:D="DAV:"> - <D:response> - <D:href>/container/</D:href> - <D:propstat> - <D:prop xmlns:R="http://ns.example.com/boxschema/"> - <R:bigbox><R:BoxType>Box type A</R:BoxType></R:bigbox> - <R:author><R:Name>Hadrian</R:Name></R:author> - <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate> - <D:displayname>Example collection</D:displayname> - <D:resourcetype><D:collection/></D:resourcetype> - <D:supportedlock> - <D:lockentry> - <D:lockscope><D:exclusive/></D:lockscope> - <D:locktype><D:write/></D:locktype> - </D:lockentry> - <D:lockentry> - <D:lockscope><D:shared/></D:lockscope> - <D:locktype><D:write/></D:locktype> - </D:lockentry> - </D:supportedlock> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> - <D:response> - <D:href>/container/front.html</D:href> - <D:propstat> - <D:prop xmlns:R="http://ns.example.com/boxschema/"> - <R:bigbox><R:BoxType>Box type B</R:BoxType> - </R:bigbox> - <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate> - <D:displayname>Example HTML resource</D:displayname> - <D:getcontentlength>4525</D:getcontentlength> - <D:getcontenttype>text/html</D:getcontenttype> - <D:getetag>"zzyzx"</D:getetag> - <D:getlastmodified - >Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified> - <D:resourcetype/> - <D:supportedlock> - <D:lockentry> - <D:lockscope><D:exclusive/></D:lockscope> - <D:locktype><D:write/></D:locktype> - </D:lockentry> - <D:lockentry> - <D:lockscope><D:shared/></D:lockscope> - <D:locktype><D:write/></D:locktype> - </D:lockentry> - </D:supportedlock> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> - </D:multistatus>"#; - - let mut rdr = Reader::new(NsReader::from_reader(src.as_bytes())).await.unwrap(); - let got = rdr.find::<Multistatus::<Core, PropValue<Core>>>().await.unwrap(); - - assert_eq!(got, Multistatus { - responses: vec![ - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("/container/".into()), - vec![PropStat { - prop: PropValue(vec![ - Property::CreationDate(FixedOffset::west_opt(8 * 3600).unwrap().with_ymd_and_hms(1997, 12, 01, 17, 42, 21).unwrap()), - Property::DisplayName("Example collection".into()), - Property::ResourceType(vec![ResourceType::Collection]), - Property::SupportedLock(vec![ - LockEntry { - lockscope: LockScope::Exclusive, - locktype: LockType::Write, - }, - LockEntry { - lockscope: LockScope::Shared, - locktype: LockType::Write, - }, - ]), - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }], - ), - error: None, - responsedescription: None, - location: None, - - }, - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("/container/front.html".into()), - vec![PropStat { - prop: PropValue(vec![ - Property::CreationDate(FixedOffset::west_opt(8 * 3600).unwrap().with_ymd_and_hms(1997, 12, 01, 18, 27, 21).unwrap()), - Property::DisplayName("Example HTML resource".into()), - Property::GetContentLength(4525), - Property::GetContentType("text/html".into()), - Property::GetEtag(r#""zzyzx""#.into()), - Property::GetLastModified(FixedOffset::west_opt(0).unwrap().with_ymd_and_hms(1998, 01, 12, 09, 25, 56).unwrap()), - //Property::ResourceType(vec![]), - Property::SupportedLock(vec![ - LockEntry { - lockscope: LockScope::Exclusive, - locktype: LockType::Write, - }, - LockEntry { - lockscope: LockScope::Shared, - locktype: LockType::Write, - }, - ]), - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }], - ), - error: None, - responsedescription: None, - location: None, - - }, - ], - responsedescription: None, - }); - } - -} diff --git a/src/dav/encoder.rs b/src/dav/encoder.rs deleted file mode 100644 index 4de5440..0000000 --- a/src/dav/encoder.rs +++ /dev/null @@ -1,1117 +0,0 @@ -use std::io::Cursor; - -use quick_xml::Error as QError; -use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText}; -use quick_xml::writer::ElementWriter; -use quick_xml::name::PrefixDeclaration; -use tokio::io::AsyncWrite; -use super::types::*; -use super::xml::{Node, Writer,QWrite,IWrite}; - - -// --- XML ROOTS - -/// PROPFIND REQUEST -impl<E: Extension> QWrite for PropFind<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("propfind"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - match self { - Self::PropName => { - let empty_propname = xml.create_dav_element("propname"); - xml.q.write_event_async(Event::Empty(empty_propname)).await? - }, - Self::AllProp(maybe_include) => { - let empty_allprop = xml.create_dav_element("allprop"); - xml.q.write_event_async(Event::Empty(empty_allprop)).await?; - if let Some(include) = maybe_include { - include.qwrite(xml).await?; - } - }, - Self::Prop(propname) => propname.qwrite(xml).await?, - } - xml.q.write_event_async(Event::End(end)).await - } -} - -/// PROPPATCH REQUEST -impl<E: Extension> QWrite for PropertyUpdate<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("propertyupdate"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for update in self.0.iter() { - update.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - - -/// PROPFIND RESPONSE, PROPPATCH RESPONSE, COPY RESPONSE, MOVE RESPONSE -/// DELETE RESPONSE, -impl<E: Extension, N: Node<N>> QWrite for Multistatus<E,N> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("multistatus"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for response in self.responses.iter() { - response.qwrite(xml).await?; - } - if let Some(description) = &self.responsedescription { - description.qwrite(xml).await?; - } - - xml.q.write_event_async(Event::End(end)).await?; - Ok(()) - } -} - -/// LOCK REQUEST -impl QWrite for LockInfo { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("lockinfo"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.lockscope.qwrite(xml).await?; - self.locktype.qwrite(xml).await?; - if let Some(owner) = &self.owner { - owner.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -/// SOME LOCK RESPONSES -impl<E: Extension> QWrite for PropValue<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("prop"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for propval in &self.0 { - propval.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -/// Error response -impl<E: Extension> QWrite for Error<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("error"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for violation in &self.0 { - violation.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -// --- XML inner elements -impl<E: Extension> QWrite for PropertyUpdateItem<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::Set(set) => set.qwrite(xml).await, - Self::Remove(rm) => rm.qwrite(xml).await, - } - } -} - -impl<E: Extension> QWrite for Set<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("set"); - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension> QWrite for Remove<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("remove"); - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - - -impl<E: Extension> QWrite for PropName<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("prop"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for propname in &self.0 { - propname.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - - -impl QWrite for Href { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("href"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(&self.0))).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension, N: Node<N>> QWrite for Response<E,N> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("response"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.status_or_propstat.qwrite(xml).await?; - if let Some(error) = &self.error { - error.qwrite(xml).await?; - } - if let Some(responsedescription) = &self.responsedescription { - responsedescription.qwrite(xml).await?; - } - if let Some(location) = &self.location { - location.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension, N: Node<N>> QWrite for StatusOrPropstat<E,N> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::Status(many_href, status) => { - for href in many_href.iter() { - href.qwrite(xml).await?; - } - status.qwrite(xml).await - }, - Self::PropStat(href, propstat_list) => { - href.qwrite(xml).await?; - for propstat in propstat_list.iter() { - propstat.qwrite(xml).await?; - } - Ok(()) - } - } - } -} - -impl QWrite for Status { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("status"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - - let txt = format!("HTTP/1.1 {} {}", self.0.as_str(), self.0.canonical_reason().unwrap_or("No reason")); - xml.q.write_event_async(Event::Text(BytesText::new(&txt))).await?; - - xml.q.write_event_async(Event::End(end)).await?; - - Ok(()) - } -} - -impl QWrite for ResponseDescription { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("responsedescription"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(&self.0))).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for Location { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("location"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension, N: Node<N>> QWrite for PropStat<E,N> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("propstat"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.prop.qwrite(xml).await?; - self.status.qwrite(xml).await?; - if let Some(error) = &self.error { - error.qwrite(xml).await?; - } - if let Some(description) = &self.responsedescription { - description.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await?; - - Ok(()) - } -} - -impl<E: Extension> QWrite for Property<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - use Property::*; - match self { - CreationDate(date) => { - // <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate> - let start = xml.create_dav_element("creationdate"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(&date.to_rfc3339()))).await?; - xml.q.write_event_async(Event::End(end)).await?; - }, - DisplayName(name) => { - // <D:displayname>Example collection</D:displayname> - let start = xml.create_dav_element("displayname"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(name))).await?; - xml.q.write_event_async(Event::End(end)).await?; - }, - GetContentLanguage(lang) => { - let start = xml.create_dav_element("getcontentlanguage"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(lang))).await?; - xml.q.write_event_async(Event::End(end)).await?; - }, - GetContentLength(len) => { - // <D:getcontentlength>4525</D:getcontentlength> - let start = xml.create_dav_element("getcontentlength"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(&len.to_string()))).await?; - xml.q.write_event_async(Event::End(end)).await?; - }, - GetContentType(ct) => { - // <D:getcontenttype>text/html</D:getcontenttype> - let start = xml.create_dav_element("getcontenttype"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(&ct))).await?; - xml.q.write_event_async(Event::End(end)).await?; - }, - GetEtag(et) => { - // <D:getetag>"zzyzx"</D:getetag> - let start = xml.create_dav_element("getetag"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(et))).await?; - xml.q.write_event_async(Event::End(end)).await?; - }, - GetLastModified(date) => { - // <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 GMT</D:getlastmodified> - let start = xml.create_dav_element("getlastmodified"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - xml.q.write_event_async(Event::Text(BytesText::new(&date.to_rfc2822()))).await?; - xml.q.write_event_async(Event::End(end)).await?; - }, - LockDiscovery(many_locks) => { - // <D:lockdiscovery><D:activelock> ... </D:activelock></D:lockdiscovery> - let start = xml.create_dav_element("lockdiscovery"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for lock in many_locks.iter() { - lock.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await?; - }, - ResourceType(many_types) => { - // <D:resourcetype><D:collection/></D:resourcetype> - - // <D:resourcetype/> - - // <x:resourcetype xmlns:x="DAV:"> - // <x:collection/> - // <f:search-results xmlns:f="http://www.example.com/ns"/> - // </x:resourcetype> - - let start = xml.create_dav_element("resourcetype"); - if many_types.is_empty() { - xml.q.write_event_async(Event::Empty(start)).await?; - } else { - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - for restype in many_types.iter() { - restype.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await?; - } - }, - SupportedLock(many_entries) => { - // <D:supportedlock/> - - // <D:supportedlock> <D:lockentry> ... </D:lockentry> </D:supportedlock> - - let start = xml.create_dav_element("supportedlock"); - if many_entries.is_empty() { - xml.q.write_event_async(Event::Empty(start)).await?; - } else { - let end = start.to_end(); - xml.q.write_event_async(Event::Start(start.clone())).await?; - for entry in many_entries.iter() { - entry.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await?; - } - }, - Extension(inner) => inner.qwrite(xml).await?, - }; - Ok(()) - } -} - -impl<E: Extension> QWrite for ResourceType<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - match self { - Self::Collection => { - let empty_collection = xml.create_dav_element("collection"); - xml.q.write_event_async(Event::Empty(empty_collection)).await - }, - Self::Extension(inner) => inner.qwrite(xml).await, - } - } -} - -impl<E: Extension> QWrite for Include<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("include"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for prop in self.0.iter() { - prop.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension> QWrite for PropertyRequest<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - use PropertyRequest::*; - let mut atom = async |c| { - let empty_tag = xml.create_dav_element(c); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }; - - match self { - CreationDate => atom("creationdate").await, - DisplayName => atom("displayname").await, - GetContentLanguage => atom("getcontentlanguage").await, - GetContentLength => atom("getcontentlength").await, - GetContentType => atom("getcontenttype").await, - GetEtag => atom("getetag").await, - GetLastModified => atom("getlastmodified").await, - LockDiscovery => atom("lockdiscovery").await, - ResourceType => atom("resourcetype").await, - SupportedLock => atom("supportedlock").await, - Extension(inner) => inner.qwrite(xml).await, - } - } -} - -impl QWrite for ActiveLock { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - // <D:activelock> - // <D:locktype><D:write/></D:locktype> - // <D:lockscope><D:exclusive/></D:lockscope> - // <D:depth>infinity</D:depth> - // <D:owner> - // <D:href>http://example.org/~ejw/contact.html</D:href> - // </D:owner> - // <D:timeout>Second-604800</D:timeout> - // <D:locktoken> - // <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href> - // </D:locktoken> - // <D:lockroot> - // <D:href>http://example.com/workspace/webdav/proposal.doc</D:href> - // </D:lockroot> - // </D:activelock> - let start = xml.create_dav_element("activelock"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.locktype.qwrite(xml).await?; - self.lockscope.qwrite(xml).await?; - self.depth.qwrite(xml).await?; - if let Some(owner) = &self.owner { - owner.qwrite(xml).await?; - } - if let Some(timeout) = &self.timeout { - timeout.qwrite(xml).await?; - } - if let Some(locktoken) = &self.locktoken { - locktoken.qwrite(xml).await?; - } - self.lockroot.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for LockType { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("locktype"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - match self { - Self::Write => { - let empty_write = xml.create_dav_element("write"); - xml.q.write_event_async(Event::Empty(empty_write)).await? - }, - }; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for LockScope { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("lockscope"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - match self { - Self::Exclusive => { - let empty_tag = xml.create_dav_element("exclusive"); - xml.q.write_event_async(Event::Empty(empty_tag)).await? - }, - Self::Shared => { - let empty_tag = xml.create_dav_element("shared"); - xml.q.write_event_async(Event::Empty(empty_tag)).await? - }, - }; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for Owner { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("owner"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - match self { - Self::Txt(txt) => xml.q.write_event_async(Event::Text(BytesText::new(&txt))).await?, - Self::Href(href) => href.qwrite(xml).await?, - Self::Unknown => (), - } - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for Depth { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("depth"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - match self { - Self::Zero => xml.q.write_event_async(Event::Text(BytesText::new("0"))).await?, - Self::One => xml.q.write_event_async(Event::Text(BytesText::new("1"))).await?, - Self::Infinity => xml.q.write_event_async(Event::Text(BytesText::new("infinity"))).await?, - }; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for Timeout { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("timeout"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - match self { - Self::Seconds(count) => { - let txt = format!("Second-{}", count); - xml.q.write_event_async(Event::Text(BytesText::new(&txt))).await? - }, - Self::Infinite => xml.q.write_event_async(Event::Text(BytesText::new("Infinite"))).await? - }; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for LockToken { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("locktoken"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for LockRoot { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("lockroot"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.0.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl QWrite for LockEntry { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let start = xml.create_dav_element("lockentry"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - self.lockscope.qwrite(xml).await?; - self.locktype.qwrite(xml).await?; - xml.q.write_event_async(Event::End(end)).await - } -} - -impl<E: Extension> QWrite for Violation<E> { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> { - let mut atom = async |c| { - let empty_tag = xml.create_dav_element(c); - xml.q.write_event_async(Event::Empty(empty_tag)).await - }; - - match self { - Violation::LockTokenMatchesRequestUri => atom("lock-token-matches-request-uri").await, - Violation::LockTokenSubmitted(hrefs) if hrefs.is_empty() => atom("lock-token-submitted").await, - Violation::LockTokenSubmitted(hrefs) => { - let start = xml.create_dav_element("lock-token-submitted"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for href in hrefs { - href.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - }, - Violation::NoConflictingLock(hrefs) if hrefs.is_empty() => atom("no-conflicting-lock").await, - Violation::NoConflictingLock(hrefs) => { - let start = xml.create_dav_element("no-conflicting-lock"); - let end = start.to_end(); - - xml.q.write_event_async(Event::Start(start.clone())).await?; - for href in hrefs { - href.qwrite(xml).await?; - } - xml.q.write_event_async(Event::End(end)).await - }, - Violation::NoExternalEntities => atom("no-external-entities").await, - Violation::PreservedLiveProperties => atom("preserved-live-properties").await, - Violation::PropfindFiniteDepth => atom("propfind-finite-depth").await, - Violation::CannotModifyProtectedProperty => atom("cannot-modify-protected-property").await, - Violation::Extension(inner) => inner.qwrite(xml).await, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::dav::realization::Core; - use tokio::io::AsyncWriteExt; - - /// To run only the unit tests and avoid the behavior ones: - /// cargo test --bin aerogramme - - async fn serialize(elem: &impl QWrite) -> String { - let mut buffer = Vec::new(); - let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); - let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4); - let ns_to_apply = vec![ ("xmlns:D".into(), "DAV:".into()) ]; - let mut writer = Writer { q, ns_to_apply }; - - elem.qwrite(&mut writer).await.expect("xml serialization"); - tokio_buffer.flush().await.expect("tokio buffer flush"); - let got = std::str::from_utf8(buffer.as_slice()).unwrap(); - - return got.into() - } - - #[tokio::test] - async fn basic_href() { - - let got = serialize( - &Href("/SOGo/dav/so/".into()) - ).await; - let expected = r#"<D:href xmlns:D="DAV:">/SOGo/dav/so/</D:href>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn basic_multistatus() { - let got = serialize( - &Multistatus::<Core, PropName<Core>> { - responses: vec![], - responsedescription: Some(ResponseDescription("Hello world".into())) - }, - ).await; - - let expected = r#"<D:multistatus xmlns:D="DAV:"> - <D:responsedescription>Hello world</D:responsedescription> -</D:multistatus>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - - #[tokio::test] - async fn rfc_error_delete_locked() { - let got = serialize( - &Error::<Core>(vec![ - Violation::LockTokenSubmitted(vec![ - Href("/locked/".into()) - ]) - ]), - ).await; - - let expected = r#"<D:error xmlns:D="DAV:"> - <D:lock-token-submitted> - <D:href>/locked/</D:href> - </D:lock-token-submitted> -</D:error>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_propname_req() { - let got = serialize( - &PropFind::<Core>::PropName, - ).await; - - let expected = r#"<D:propfind xmlns:D="DAV:"> - <D:propname/> -</D:propfind>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_propname_res() { - let got = serialize( - &Multistatus::<Core, PropName<Core>> { - responses: vec![ - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("http://www.example.com/container/".into()), - vec![PropStat { - prop: PropName(vec![ - PropertyRequest::CreationDate, - PropertyRequest::DisplayName, - PropertyRequest::ResourceType, - PropertyRequest::SupportedLock, - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }] - ), - error: None, - responsedescription: None, - location: None, - }, - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("http://www.example.com/container/front.html".into()), - vec![PropStat { - prop: PropName(vec![ - PropertyRequest::CreationDate, - PropertyRequest::DisplayName, - PropertyRequest::GetContentLength, - PropertyRequest::GetContentType, - PropertyRequest::GetEtag, - PropertyRequest::GetLastModified, - PropertyRequest::ResourceType, - PropertyRequest::SupportedLock, - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - } - ]), - error: None, - responsedescription: None, - location: None, - }, - ], - responsedescription: None, - }, - ).await; - - let expected = r#"<D:multistatus xmlns:D="DAV:"> - <D:response> - <D:href>http://www.example.com/container/</D:href> - <D:propstat> - <D:prop> - <D:creationdate/> - <D:displayname/> - <D:resourcetype/> - <D:supportedlock/> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> - <D:response> - <D:href>http://www.example.com/container/front.html</D:href> - <D:propstat> - <D:prop> - <D:creationdate/> - <D:displayname/> - <D:getcontentlength/> - <D:getcontenttype/> - <D:getetag/> - <D:getlastmodified/> - <D:resourcetype/> - <D:supportedlock/> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> -</D:multistatus>"#; - - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_allprop_req() { - let got = serialize( - &PropFind::<Core>::AllProp(None), - ).await; - - let expected = r#"<D:propfind xmlns:D="DAV:"> - <D:allprop/> -</D:propfind>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_allprop_res() { - use chrono::{DateTime,FixedOffset,TimeZone}; - let got = serialize( - &Multistatus::<Core, PropValue<Core>> { - responses: vec![ - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("/container/".into()), - vec![PropStat { - prop: PropValue(vec![ - Property::CreationDate(FixedOffset::west_opt(8 * 3600) - .unwrap() - .with_ymd_and_hms(1997, 12, 1, 17, 42, 21) - .unwrap()), - Property::DisplayName("Example collection".into()), - Property::ResourceType(vec![ResourceType::Collection]), - Property::SupportedLock(vec![ - LockEntry { - lockscope: LockScope::Exclusive, - locktype: LockType::Write, - }, - LockEntry { - lockscope: LockScope::Shared, - locktype: LockType::Write, - }, - ]), - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }] - ), - error: None, - responsedescription: None, - location: None, - }, - Response { - status_or_propstat: StatusOrPropstat::PropStat( - Href("/container/front.html".into()), - vec![PropStat { - prop: PropValue(vec![ - Property::CreationDate(FixedOffset::west_opt(8 * 3600) - .unwrap() - .with_ymd_and_hms(1997, 12, 1, 18, 27, 21) - .unwrap()), - Property::DisplayName("Example HTML resource".into()), - Property::GetContentLength(4525), - Property::GetContentType("text/html".into()), - Property::GetEtag(r#""zzyzx""#.into()), - Property::GetLastModified(FixedOffset::east_opt(0) - .unwrap() - .with_ymd_and_hms(1998, 1, 12, 9, 25, 56) - .unwrap()), - Property::ResourceType(vec![]), - Property::SupportedLock(vec![ - LockEntry { - lockscope: LockScope::Exclusive, - locktype: LockType::Write, - }, - LockEntry { - lockscope: LockScope::Shared, - locktype: LockType::Write, - }, - ]), - ]), - status: Status(http::status::StatusCode::OK), - error: None, - responsedescription: None, - }] - ), - error: None, - responsedescription: None, - location: None, - }, - ], - responsedescription: None, - } - ).await; - - let expected = r#"<D:multistatus xmlns:D="DAV:"> - <D:response> - <D:href>/container/</D:href> - <D:propstat> - <D:prop> - <D:creationdate>1997-12-01T17:42:21-08:00</D:creationdate> - <D:displayname>Example collection</D:displayname> - <D:resourcetype> - <D:collection/> - </D:resourcetype> - <D:supportedlock> - <D:lockentry> - <D:lockscope> - <D:exclusive/> - </D:lockscope> - <D:locktype> - <D:write/> - </D:locktype> - </D:lockentry> - <D:lockentry> - <D:lockscope> - <D:shared/> - </D:lockscope> - <D:locktype> - <D:write/> - </D:locktype> - </D:lockentry> - </D:supportedlock> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> - <D:response> - <D:href>/container/front.html</D:href> - <D:propstat> - <D:prop> - <D:creationdate>1997-12-01T18:27:21-08:00</D:creationdate> - <D:displayname>Example HTML resource</D:displayname> - <D:getcontentlength>4525</D:getcontentlength> - <D:getcontenttype>text/html</D:getcontenttype> - <D:getetag>"zzyzx"</D:getetag> - <D:getlastmodified>Mon, 12 Jan 1998 09:25:56 +0000</D:getlastmodified> - <D:resourcetype/> - <D:supportedlock> - <D:lockentry> - <D:lockscope> - <D:exclusive/> - </D:lockscope> - <D:locktype> - <D:write/> - </D:locktype> - </D:lockentry> - <D:lockentry> - <D:lockscope> - <D:shared/> - </D:lockscope> - <D:locktype> - <D:write/> - </D:locktype> - </D:lockentry> - </D:supportedlock> - </D:prop> - <D:status>HTTP/1.1 200 OK</D:status> - </D:propstat> - </D:response> -</D:multistatus>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_allprop_include() { - let got = serialize( - &PropFind::<Core>::AllProp(Some(Include(vec![ - PropertyRequest::DisplayName, - PropertyRequest::ResourceType, - ]))), - ).await; - - let expected = r#"<D:propfind xmlns:D="DAV:"> - <D:allprop/> - <D:include> - <D:displayname/> - <D:resourcetype/> - </D:include> -</D:propfind>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_propertyupdate() { - let got = serialize( - &PropertyUpdate::<Core>(vec![ - PropertyUpdateItem::Set(Set(PropValue(vec![ - Property::GetContentLanguage("fr-FR".into()), - ]))), - PropertyUpdateItem::Remove(Remove(PropName(vec![ - PropertyRequest::DisplayName, - ]))), - ]), - ).await; - - let expected = r#"<D:propertyupdate xmlns:D="DAV:"> - <D:set> - <D:prop> - <D:getcontentlanguage>fr-FR</D:getcontentlanguage> - </D:prop> - </D:set> - <D:remove> - <D:prop> - <D:displayname/> - </D:prop> - </D:remove> -</D:propertyupdate>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_delete_locked2() { - let got = serialize( - &Multistatus::<Core, PropValue<Core>> { - responses: vec![Response { - status_or_propstat: StatusOrPropstat::Status( - vec![Href("http://www.example.com/container/resource3".into())], - Status(http::status::StatusCode::from_u16(423).unwrap()) - ), - error: Some(Error(vec![Violation::LockTokenSubmitted(vec![])])), - responsedescription: None, - location: None, - }], - responsedescription: None, - }, - ).await; - - let expected = r#"<D:multistatus xmlns:D="DAV:"> - <D:response> - <D:href>http://www.example.com/container/resource3</D:href> - <D:status>HTTP/1.1 423 Locked</D:status> - <D:error> - <D:lock-token-submitted/> - </D:error> - </D:response> -</D:multistatus>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_simple_lock_request() { - let got = serialize( - &LockInfo { - lockscope: LockScope::Exclusive, - locktype: LockType::Write, - owner: Some(Owner::Href(Href("http://example.org/~ejw/contact.html".into()))), - }, - ).await; - - let expected = r#"<D:lockinfo xmlns:D="DAV:"> - <D:lockscope> - <D:exclusive/> - </D:lockscope> - <D:locktype> - <D:write/> - </D:locktype> - <D:owner> - <D:href>http://example.org/~ejw/contact.html</D:href> - </D:owner> -</D:lockinfo>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } - - #[tokio::test] - async fn rfc_simple_lock_response() { - let got = serialize( - &PropValue::<Core>(vec![ - Property::LockDiscovery(vec![ActiveLock { - lockscope: LockScope::Exclusive, - locktype: LockType::Write, - depth: Depth::Infinity, - owner: Some(Owner::Href(Href("http://example.org/~ejw/contact.html".into()))), - timeout: Some(Timeout::Seconds(604800)), - locktoken: Some(LockToken(Href("urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4".into()))), - lockroot: LockRoot(Href("http://example.com/workspace/webdav/proposal.doc".into())), - }]), - ]), - ).await; - - let expected = r#"<D:prop xmlns:D="DAV:"> - <D:lockdiscovery> - <D:activelock> - <D:locktype> - <D:write/> - </D:locktype> - <D:lockscope> - <D:exclusive/> - </D:lockscope> - <D:depth>infinity</D:depth> - <D:owner> - <D:href>http://example.org/~ejw/contact.html</D:href> - </D:owner> - <D:timeout>Second-604800</D:timeout> - <D:locktoken> - <D:href>urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4</D:href> - </D:locktoken> - <D:lockroot> - <D:href>http://example.com/workspace/webdav/proposal.doc</D:href> - </D:lockroot> - </D:activelock> - </D:lockdiscovery> -</D:prop>"#; - - assert_eq!(&got, expected, "\n---GOT---\n{got}\n---EXP---\n{expected}\n"); - } -} diff --git a/src/dav/error.rs b/src/dav/error.rs deleted file mode 100644 index 78c6d6b..0000000 --- a/src/dav/error.rs +++ /dev/null @@ -1,42 +0,0 @@ -use quick_xml::events::attributes::AttrError; - -#[derive(Debug)] -pub enum ParsingError { - Recoverable, - MissingChild, - NamespacePrefixAlreadyUsed, - WrongToken, - TagNotFound, - InvalidValue, - Utf8Error(std::str::Utf8Error), - QuickXml(quick_xml::Error), - Chrono(chrono::format::ParseError), - Int(std::num::ParseIntError), - Eof -} -impl From<AttrError> for ParsingError { - fn from(value: AttrError) -> Self { - Self::QuickXml(value.into()) - } -} -impl From<quick_xml::Error> for ParsingError { - fn from(value: quick_xml::Error) -> Self { - Self::QuickXml(value) - } -} -impl From<std::str::Utf8Error> for ParsingError { - fn from(value: std::str::Utf8Error) -> Self { - Self::Utf8Error(value) - } -} -impl From<chrono::format::ParseError> for ParsingError { - fn from(value: chrono::format::ParseError) -> Self { - Self::Chrono(value) - } -} - -impl From<std::num::ParseIntError> for ParsingError { - fn from(value: std::num::ParseIntError) -> Self { - Self::Int(value) - } -} diff --git a/src/dav/mod.rs b/src/dav/mod.rs deleted file mode 100644 index 906cfdd..0000000 --- a/src/dav/mod.rs +++ /dev/null @@ -1,167 +0,0 @@ -// utils -pub mod error; -pub mod xml; - -// webdav -pub mod types; -pub mod encoder; -pub mod decoder; - -// calendar -mod caltypes; -mod calencoder; -mod caldecoder; - -// wip -mod acltypes; -mod versioningtypes; - -// final type -pub mod realization; - - -use std::net::SocketAddr; - -use anyhow::{anyhow, Result}; -use base64::Engine; -use hyper::service::service_fn; -use hyper::{Request, Response, body::Bytes}; -use hyper::server::conn::http1 as http; -use hyper_util::rt::TokioIo; -use http_body_util::Full; -use futures::stream::{FuturesUnordered, StreamExt}; -use tokio::net::TcpListener; -use tokio::sync::watch; - -use crate::config::DavUnsecureConfig; -use crate::login::ArcLoginProvider; -use crate::user::User; - -pub struct Server { - bind_addr: SocketAddr, - login_provider: ArcLoginProvider, -} - -pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server { - Server { - bind_addr: config.bind_addr, - login_provider: login, - } -} - -impl Server { - pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - tracing::info!("DAV server listening on {:#}", self.bind_addr); - - let mut connections = FuturesUnordered::new(); - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - let (socket, remote_addr) = tokio::select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - tracing::info!("Accepted connection from {}", remote_addr); - let stream = TokioIo::new(socket); - let login = self.login_provider.clone(); - let conn = tokio::spawn(async move { - //@FIXME should create a generic "public web" server on which "routers" could be - //abitrarily bound - //@FIXME replace with a handler supporting http2 and TLS - match http::Builder::new().serve_connection(stream, service_fn(|req: Request<hyper::body::Incoming>| { - let login = login.clone(); - async move { - auth(login, req).await - } - })).await { - Err(e) => tracing::warn!(err=?e, "connection failed"), - Ok(()) => tracing::trace!("connection terminated with success"), - } - }); - connections.push(conn); - } - drop(tcp); - - tracing::info!("Server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -//@FIXME We should not support only BasicAuth -async fn auth( - login: ArcLoginProvider, - req: Request<impl hyper::body::Body>, -) -> Result<Response<Full<Bytes>>> { - - let auth_val = match req.headers().get("Authorization") { - Some(hv) => hv.to_str()?, - None => return Ok(Response::builder() - .status(401) - .body(Full::new(Bytes::from("Missing Authorization field")))?), - }; - - let b64_creds_maybe_padded = match auth_val.split_once(" ") { - Some(("Basic", b64)) => b64, - _ => return Ok(Response::builder() - .status(400) - .body(Full::new(Bytes::from("Unsupported Authorization field")))?), - }; - - // base64urlencoded may have trailing equals, base64urlsafe has not - // theoretically authorization is padded but "be liberal in what you accept" - let b64_creds_clean = b64_creds_maybe_padded.trim_end_matches('='); - - // Decode base64 - let creds = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64_creds_clean)?; - let str_creds = std::str::from_utf8(&creds)?; - - // Split username and password - let (username, password) = str_creds - .split_once(':') - .ok_or(anyhow!("Missing colon in Authorization, can't split decoded value into a username/password pair"))?; - - // Call login provider - let creds = match login.login(username, password).await { - Ok(c) => c, - Err(e) => return Ok(Response::builder() - .status(401) - .body(Full::new(Bytes::from("Wrong credentials")))?), - }; - - // Build a user - let user = User::new(username.into(), creds).await?; - - // Call router with user - router(user, req).await -} - -async fn router(user: std::sync::Arc<User>, req: Request<impl hyper::body::Body>) -> Result<Response<Full<Bytes>>> { - let path_segments: Vec<_> = req.uri().path().split("/").filter(|s| *s != "").collect(); - match path_segments.as_slice() { - [] => tracing::info!("root"), - [ username, ..] if *username != user.username => return Ok(Response::builder() - .status(403) - .body(Full::new(Bytes::from("Accessing other user ressources is not allowed")))?), - [ _ ] => tracing::info!("user home"), - [ _, "calendar" ] => tracing::info!("user calendars"), - [ _, "calendar", colname ] => tracing::info!(name=colname, "selected calendar"), - [ _, "calendar", colname, member ] => tracing::info!(name=colname, obj=member, "selected event"), - _ => return Ok(Response::builder() - .status(404) - .body(Full::new(Bytes::from("Resource not found")))?), - } - Ok(Response::new(Full::new(Bytes::from("Hello World!")))) -} - -async fn collections(user: std::sync::Arc<User>, req: Request<impl hyper::body::Body>) -> Result<Response<Full<Bytes>>> { - unimplemented!(); -} diff --git a/src/dav/realization.rs b/src/dav/realization.rs deleted file mode 100644 index 33a556e..0000000 --- a/src/dav/realization.rs +++ /dev/null @@ -1,42 +0,0 @@ -use super::types as dav; -use super::caltypes as cal; -use super::xml; -use super::error; - -#[derive(Debug, PartialEq)] -pub struct Disabled(()); -impl xml::QRead<Disabled> for Disabled { - async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> { - Err(error::ParsingError::Recoverable) - } -} -impl xml::QWrite for Disabled { - async fn qwrite(&self, xml: &mut xml::Writer<impl xml::IWrite>) -> Result<(), quick_xml::Error> { - unreachable!(); - } -} - -/// The base WebDAV -/// -/// Any extension is kooh is disabled through an object we can't build -/// due to a private inner element. -#[derive(Debug, PartialEq)] -pub struct Core {} -impl dav::Extension for Core { - type Error = Disabled; - type Property = Disabled; - type PropertyRequest = Disabled; - type ResourceType = Disabled; -} - -// WebDAV with the base Calendar implementation (RFC4791) -#[derive(Debug, PartialEq)] -pub struct Calendar {} -impl dav::Extension for Calendar -{ - type Error = cal::Violation; - type Property = cal::Property; - type PropertyRequest = cal::PropertyRequest; - type ResourceType = cal::ResourceType; -} - diff --git a/src/dav/types.rs b/src/dav/types.rs deleted file mode 100644 index 5ea38d1..0000000 --- a/src/dav/types.rs +++ /dev/null @@ -1,950 +0,0 @@ -#![allow(dead_code)] -use std::fmt::Debug; - -use chrono::{DateTime,FixedOffset}; -use super::xml; -use super::error; - -/// It's how we implement a DAV extension -/// (That's the dark magic part...) -pub trait Extension: std::fmt::Debug + PartialEq { - type Error: xml::Node<Self::Error>; - type Property: xml::Node<Self::Property>; - type PropertyRequest: xml::Node<Self::PropertyRequest>; - type ResourceType: xml::Node<Self::ResourceType>; -} - -/// 14.1. activelock XML Element -/// -/// Name: activelock -/// -/// Purpose: Describes a lock on a resource. -/// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?, -/// locktoken?, lockroot)> -#[derive(Debug, PartialEq)] -pub struct ActiveLock { - pub lockscope: LockScope, - pub locktype: LockType, - pub depth: Depth, - pub owner: Option<Owner>, - pub timeout: Option<Timeout>, - pub locktoken: Option<LockToken>, - pub lockroot: LockRoot, -} - -/// 14.3 collection XML Element -/// -/// Name: collection -/// -/// Purpose: Identifies the associated resource as a collection. The -/// DAV:resourcetype property of a collection resource MUST contain -/// this element. It is normally empty but extensions may add sub- -/// elements. -/// -/// <!ELEMENT collection EMPTY > -#[derive(Debug, PartialEq)] -pub struct Collection{} - -/// 14.4 depth XML Element -/// -/// Name: depth -/// -/// Purpose: Used for representing depth values in XML content (e.g., -/// in lock information). -/// -/// Value: "0" | "1" | "infinity" -/// -/// <!ELEMENT depth (#PCDATA) > -#[derive(Debug, PartialEq)] -pub enum Depth { - Zero, - One, - Infinity -} - -/// 14.5 error XML Element -/// -/// Name: error -/// -/// Purpose: Error responses, particularly 403 Forbidden and 409 -/// Conflict, sometimes need more information to indicate what went -/// wrong. In these cases, servers MAY return an XML response body -/// with a document element of 'error', containing child elements -/// identifying particular condition codes. -/// -/// Description: Contains at least one XML element, and MUST NOT -/// contain text or mixed content. Any element that is a child of the -/// 'error' element is considered to be a precondition or -/// postcondition code. Unrecognized elements MUST be ignored. -/// -/// <!ELEMENT error ANY > -#[derive(Debug, PartialEq)] -pub struct Error<E: Extension>(pub Vec<Violation<E>>); -#[derive(Debug, PartialEq)] -pub enum Violation<E: Extension> { - /// Name: lock-token-matches-request-uri - /// - /// Use with: 409 Conflict - /// - /// Purpose: (precondition) -- A request may include a Lock-Token header - /// to identify a lock for the UNLOCK method. However, if the - /// Request-URI does not fall within the scope of the lock identified - /// by the token, the server SHOULD use this error. The lock may have - /// a scope that does not include the Request-URI, or the lock could - /// have disappeared, or the token may be invalid. - LockTokenMatchesRequestUri, - - /// Name: lock-token-submitted (precondition) - /// - /// Use with: 423 Locked - /// - /// Purpose: The request could not succeed because a lock token should - /// have been submitted. This element, if present, MUST contain at - /// least one URL of a locked resource that prevented the request. In - /// cases of MOVE, COPY, and DELETE where collection locks are - /// involved, it can be difficult for the client to find out which - /// locked resource made the request fail -- but the server is only - /// responsible for returning one such locked resource. The server - /// MAY return every locked resource that prevented the request from - /// succeeding if it knows them all. - /// - /// <!ELEMENT lock-token-submitted (href+) > - LockTokenSubmitted(Vec<Href>), - - /// Name: no-conflicting-lock (precondition) - /// - /// Use with: Typically 423 Locked - /// - /// Purpose: A LOCK request failed due the presence of an already - /// existing conflicting lock. Note that a lock can be in conflict - /// although the resource to which the request was directed is only - /// indirectly locked. In this case, the precondition code can be - /// used to inform the client about the resource that is the root of - /// the conflicting lock, avoiding a separate lookup of the - /// "lockdiscovery" property. - /// - /// <!ELEMENT no-conflicting-lock (href)* > - NoConflictingLock(Vec<Href>), - - /// Name: no-external-entities - /// - /// Use with: 403 Forbidden - /// - /// Purpose: (precondition) -- If the server rejects a client request - /// because the request body contains an external entity, the server - /// SHOULD use this error. - NoExternalEntities, - - /// Name: preserved-live-properties - /// - /// Use with: 409 Conflict - /// - /// Purpose: (postcondition) -- The server received an otherwise-valid - /// MOVE or COPY request, but cannot maintain the live properties with - /// the same behavior at the destination. It may be that the server - /// only supports some live properties in some parts of the - /// repository, or simply has an internal error. - PreservedLiveProperties, - - /// Name: propfind-finite-depth - /// - /// Use with: 403 Forbidden - /// - /// Purpose: (precondition) -- This server does not allow infinite-depth - /// PROPFIND requests on collections. - PropfindFiniteDepth, - - - /// Name: cannot-modify-protected-property - /// - /// Use with: 403 Forbidden - /// - /// Purpose: (precondition) -- The client attempted to set a protected - /// property in a PROPPATCH (such as DAV:getetag). See also - /// [RFC3253], Section 3.12. - CannotModifyProtectedProperty, - - /// Specific errors - Extension(E::Error), -} - -/// 14.6. exclusive XML Element -/// -/// Name: exclusive -/// -/// Purpose: Specifies an exclusive lock. -/// -/// <!ELEMENT exclusive EMPTY > -#[derive(Debug, PartialEq)] -pub struct Exclusive {} - -/// 14.7. href XML Element -/// -/// Name: href -/// -/// Purpose: MUST contain a URI or a relative reference. -/// -/// Description: There may be limits on the value of 'href' depending -/// on the context of its use. Refer to the specification text where -/// 'href' is used to see what limitations apply in each case. -/// -/// Value: Simple-ref -/// -/// <!ELEMENT href (#PCDATA)> -#[derive(Debug, PartialEq)] -pub struct Href(pub String); - - -/// 14.8. include XML Element -/// -/// Name: include -/// -/// Purpose: Any child element represents the name of a property to be -/// included in the PROPFIND response. All elements inside an -/// 'include' XML element MUST define properties related to the -/// resource, although possible property names are in no way limited -/// to those property names defined in this document or other -/// standards. This element MUST NOT contain text or mixed content. -/// -/// <!ELEMENT include ANY > -#[derive(Debug, PartialEq)] -pub struct Include<E: Extension>(pub Vec<PropertyRequest<E>>); - -/// 14.9. location XML Element -/// -/// Name: location -/// -/// Purpose: HTTP defines the "Location" header (see [RFC2616], Section -/// 14.30) for use with some status codes (such as 201 and the 300 -/// series codes). When these codes are used inside a 'multistatus' -/// element, the 'location' element can be used to provide the -/// accompanying Location header value. -/// -/// Description: Contains a single href element with the same value -/// that would be used in a Location header. -/// -/// <!ELEMENT location (href)> -#[derive(Debug, PartialEq)] -pub struct Location(pub Href); - -/// 14.10. lockentry XML Element -/// -/// Name: lockentry -/// -/// Purpose: Defines the types of locks that can be used with the -/// resource. -/// -/// <!ELEMENT lockentry (lockscope, locktype) > -#[derive(Debug, PartialEq)] -pub struct LockEntry { - pub lockscope: LockScope, - pub locktype: LockType, -} - -/// 14.11. lockinfo XML Element -/// -/// Name: lockinfo -/// -/// Purpose: The 'lockinfo' XML element is used with a LOCK method to -/// specify the type of lock the client wishes to have created. -/// -/// <!ELEMENT lockinfo (lockscope, locktype, owner?) > -#[derive(Debug, PartialEq)] -pub struct LockInfo { - pub lockscope: LockScope, - pub locktype: LockType, - pub owner: Option<Owner>, -} - -/// 14.12. lockroot XML Element -/// -/// Name: lockroot -/// -/// Purpose: Contains the root URL of the lock, which is the URL -/// through which the resource was addressed in the LOCK request. -/// -/// Description: The href element contains the root of the lock. The -/// server SHOULD include this in all DAV:lockdiscovery property -/// values and the response to LOCK requests. -/// -/// <!ELEMENT lockroot (href) > -#[derive(Debug, PartialEq)] -pub struct LockRoot(pub Href); - -/// 14.13. lockscope XML Element -/// -/// Name: lockscope -/// -/// Purpose: Specifies whether a lock is an exclusive lock, or a shared -/// lock. -/// <!ELEMENT lockscope (exclusive | shared) > -#[derive(Debug, PartialEq)] -pub enum LockScope { - Exclusive, - Shared -} - -/// 14.14. locktoken XML Element -/// -/// Name: locktoken -/// -/// Purpose: The lock token associated with a lock. -/// -/// Description: The href contains a single lock token URI, which -/// refers to the lock. -/// -/// <!ELEMENT locktoken (href) > -#[derive(Debug, PartialEq)] -pub struct LockToken(pub Href); - -/// 14.15. locktype XML Element -/// -/// Name: locktype -/// -/// Purpose: Specifies the access type of a lock. At present, this -/// specification only defines one lock type, the write lock. -/// -/// <!ELEMENT locktype (write) > -#[derive(Debug, PartialEq)] -pub enum LockType { - /// 14.30. write XML Element - /// - /// Name: write - /// - /// Purpose: Specifies a write lock. - /// - /// - /// <!ELEMENT write EMPTY > - Write -} - -/// 14.16. multistatus XML Element -/// -/// Name: multistatus -/// -/// Purpose: Contains multiple response messages. -/// -/// Description: The 'responsedescription' element at the top level is -/// used to provide a general message describing the overarching -/// nature of the response. If this value is available, an -/// application may use it instead of presenting the individual -/// response descriptions contained within the responses. -/// -/// <!ELEMENT multistatus (response*, responsedescription?) > -#[derive(Debug, PartialEq)] -pub struct Multistatus<E: Extension, N: xml::Node<N>> { - pub responses: Vec<Response<E, N>>, - pub responsedescription: Option<ResponseDescription>, -} - -/// 14.17. owner XML Element -/// -/// Name: owner -/// -/// Purpose: Holds client-supplied information about the creator of a -/// lock. -/// -/// Description: Allows a client to provide information sufficient for -/// either directly contacting a principal (such as a telephone number -/// or Email URI), or for discovering the principal (such as the URL -/// of a homepage) who created a lock. The value provided MUST be -/// treated as a dead property in terms of XML Information Item -/// preservation. The server MUST NOT alter the value unless the -/// owner value provided by the client is empty. For a certain amount -/// of interoperability between different client implementations, if -/// clients have URI-formatted contact information for the lock -/// creator suitable for user display, then clients SHOULD put those -/// URIs in 'href' child elements of the 'owner' element. -/// -/// Extensibility: MAY be extended with child elements, mixed content, -/// text content or attributes. -/// -/// <!ELEMENT owner ANY > -//@FIXME might need support for an extension -#[derive(Debug, PartialEq)] -pub enum Owner { - Txt(String), - Href(Href), - Unknown, -} - -/// 14.18. prop XML Element -/// -/// Name: prop -/// -/// Purpose: Contains properties related to a resource. -/// -/// Description: A generic container for properties defined on -/// resources. All elements inside a 'prop' XML element MUST define -/// properties related to the resource, although possible property -/// names are in no way limited to those property names defined in -/// this document or other standards. This element MUST NOT contain -/// text or mixed content. -/// -/// <!ELEMENT prop ANY > -#[derive(Debug, PartialEq)] -pub struct PropName<E: Extension>(pub Vec<PropertyRequest<E>>); - -#[derive(Debug, PartialEq)] -pub struct PropValue<E: Extension>(pub Vec<Property<E>>); - -/// 14.19. propertyupdate XML Element -/// -/// Name: propertyupdate -/// -/// Purpose: Contains a request to alter the properties on a resource. -/// -/// Description: This XML element is a container for the information -/// required to modify the properties on the resource. -/// -/// <!ELEMENT propertyupdate (remove | set)+ > -#[derive(Debug, PartialEq)] -pub struct PropertyUpdate<E: Extension>(pub Vec<PropertyUpdateItem<E>>); - -#[derive(Debug, PartialEq)] -pub enum PropertyUpdateItem<E: Extension> { - Remove(Remove<E>), - Set(Set<E>), -} - -/// 14.2 allprop XML Element -/// -/// Name: allprop -/// -/// Purpose: Specifies that all names and values of dead properties and -/// the live properties defined by this document existing on the -/// resource are to be returned. -/// -/// <!ELEMENT allprop EMPTY > -/// -/// --- -/// -/// 14.21. propname XML Element -/// -/// Name: propname -/// -/// Purpose: Specifies that only a list of property names on the -/// resource is to be returned. -/// -/// <!ELEMENT propname EMPTY > -/// -/// --- -/// -/// 14.20. propfind XML Element -/// -/// Name: propfind -/// -/// Purpose: Specifies the properties to be returned from a PROPFIND -/// method. Four special elements are specified for use with -/// 'propfind': 'prop', 'allprop', 'include', and 'propname'. If -/// 'prop' is used inside 'propfind', it MUST NOT contain property -/// values. -/// -/// <!ELEMENT propfind ( propname | (allprop, include?) | prop ) > -#[derive(Debug, PartialEq)] -pub enum PropFind<E: Extension> { - PropName, - AllProp(Option<Include<E>>), - Prop(PropName<E>), -} - -/// 14.22 propstat XML Element -/// -/// Name: propstat -/// -/// Purpose: Groups together a prop and status element that is -/// associated with a particular 'href' element. -/// -/// Description: The propstat XML element MUST contain one prop XML -/// element and one status XML element. The contents of the prop XML -/// element MUST only list the names of properties to which the result -/// in the status element applies. The optional precondition/ -/// postcondition element and 'responsedescription' text also apply to -/// the properties named in 'prop'. -/// -/// <!ELEMENT propstat (prop, status, error?, responsedescription?) > -#[derive(Debug, PartialEq)] -pub struct PropStat<E: Extension, N: xml::Node<N>> { - pub prop: N, - pub status: Status, - pub error: Option<Error<E>>, - pub responsedescription: Option<ResponseDescription>, -} - -/// 14.23. remove XML Element -/// -/// Name: remove -/// -/// Purpose: Lists the properties to be removed from a resource. -/// -/// Description: Remove instructs that the properties specified in prop -/// should be removed. Specifying the removal of a property that does -/// not exist is not an error. All the XML elements in a 'prop' XML -/// element inside of a 'remove' XML element MUST be empty, as only -/// the names of properties to be removed are required. -/// -/// <!ELEMENT remove (prop) > -#[derive(Debug, PartialEq)] -pub struct Remove<E: Extension>(pub PropName<E>); - -/// 14.24. response XML Element -/// -/// Name: response -/// -/// Purpose: Holds a single response describing the effect of a method -/// on resource and/or its properties. -/// -/// Description: The 'href' element contains an HTTP URL pointing to a -/// WebDAV resource when used in the 'response' container. A -/// particular 'href' value MUST NOT appear more than once as the -/// child of a 'response' XML element under a 'multistatus' XML -/// element. This requirement is necessary in order to keep -/// processing costs for a response to linear time. Essentially, this -/// prevents having to search in order to group together all the -/// responses by 'href'. There are, however, no requirements -/// regarding ordering based on 'href' values. The optional -/// precondition/postcondition element and 'responsedescription' text -/// can provide additional information about this resource relative to -/// the request or result. -/// -/// <!ELEMENT response (href, ((href*, status)|(propstat+)), -/// error?, responsedescription? , location?) > -/// -/// --- rewritten as --- -/// <!ELEMENT response ((href+, status)|(href, propstat+), error?, responsedescription?, location?> -#[derive(Debug, PartialEq)] -pub enum StatusOrPropstat<E: Extension, N: xml::Node<N>> { - // One status, multiple hrefs... - Status(Vec<Href>, Status), - // A single href, multiple properties... - PropStat(Href, Vec<PropStat<E, N>>), -} - -#[derive(Debug, PartialEq)] -pub struct Response<E: Extension, N: xml::Node<N>> { - pub status_or_propstat: StatusOrPropstat<E, N>, - pub error: Option<Error<E>>, - pub responsedescription: Option<ResponseDescription>, - pub location: Option<Location>, -} - -/// 14.25. responsedescription XML Element -/// -/// Name: responsedescription -/// -/// Purpose: Contains information about a status response within a -/// Multi-Status. -/// -/// Description: Provides information suitable to be presented to a -/// user. -/// -/// <!ELEMENT responsedescription (#PCDATA) > -#[derive(Debug, PartialEq)] -pub struct ResponseDescription(pub String); - -/// 14.26. set XML Element -/// -/// Name: set -/// -/// Purpose: Lists the property values to be set for a resource. -/// -/// Description: The 'set' element MUST contain only a 'prop' element. -/// The elements contained by the 'prop' element inside the 'set' -/// element MUST specify the name and value of properties that are set -/// on the resource identified by Request-URI. If a property already -/// exists, then its value is replaced. Language tagging information -/// appearing in the scope of the 'prop' element (in the "xml:lang" -/// attribute, if present) MUST be persistently stored along with the -/// property, and MUST be subsequently retrievable using PROPFIND. -/// -/// <!ELEMENT set (prop) > -#[derive(Debug, PartialEq)] -pub struct Set<E: Extension>(pub PropValue<E>); - -/// 14.27. shared XML Element -/// -/// Name: shared -/// -/// Purpose: Specifies a shared lock. -/// -/// -/// <!ELEMENT shared EMPTY > -#[derive(Debug, PartialEq)] -pub struct Shared {} - - -/// 14.28. status XML Element -/// -/// Name: status -/// -/// Purpose: Holds a single HTTP status-line. -/// -/// Value: status-line (defined in Section 6.1 of [RFC2616]) -/// -/// <!ELEMENT status (#PCDATA) > -//@FIXME: Better typing is possible with an enum for example -#[derive(Debug, PartialEq)] -pub struct Status(pub http::status::StatusCode); - -/// 14.29. timeout XML Element -/// -/// Name: timeout -/// -/// Purpose: The number of seconds remaining before a lock expires. -/// -/// Value: TimeType (defined in Section 10.7) -/// -/// -/// <!ELEMENT timeout (#PCDATA) > -/// -/// TimeOut = "Timeout" ":" 1#TimeType -/// TimeType = ("Second-" DAVTimeOutVal | "Infinite") -/// ; No LWS allowed within TimeType -/// DAVTimeOutVal = 1*DIGIT -/// -/// Clients MAY include Timeout request headers in their LOCK requests. -/// However, the server is not required to honor or even consider these -/// requests. Clients MUST NOT submit a Timeout request header with any -/// method other than a LOCK method. -/// -/// The "Second" TimeType specifies the number of seconds that will -/// elapse between granting of the lock at the server, and the automatic -/// removal of the lock. The timeout value for TimeType "Second" MUST -/// NOT be greater than 2^32-1. -#[derive(Debug, PartialEq)] -pub enum Timeout { - Seconds(u32), - Infinite, -} - - -/// 15. DAV Properties -/// -/// For DAV properties, the name of the property is also the same as the -/// name of the XML element that contains its value. In the section -/// below, the final line of each section gives the element type -/// declaration using the format defined in [REC-XML]. The "Value" -/// field, where present, specifies further restrictions on the allowable -/// contents of the XML element using BNF (i.e., to further restrict the -/// values of a PCDATA element). -/// -/// A protected property is one that cannot be changed with a PROPPATCH -/// request. There may be other requests that would result in a change -/// to a protected property (as when a LOCK request affects the value of -/// DAV:lockdiscovery). Note that a given property could be protected on -/// one type of resource, but not protected on another type of resource. -/// -/// A computed property is one with a value defined in terms of a -/// computation (based on the content and other properties of that -/// resource, or even of some other resource). A computed property is -/// always a protected property. -/// -/// COPY and MOVE behavior refers to local COPY and MOVE operations. -/// -/// For properties defined based on HTTP GET response headers (DAV:get*), -/// the header value could include LWS as defined in [RFC2616], Section -/// 4.2. Server implementors SHOULD strip LWS from these values before -/// using as WebDAV property values. -#[derive(Debug, PartialEq)] -pub enum PropertyRequest<E: Extension> { - CreationDate, - DisplayName, - GetContentLanguage, - GetContentLength, - GetContentType, - GetEtag, - GetLastModified, - LockDiscovery, - ResourceType, - SupportedLock, - Extension(E::PropertyRequest), -} - -#[derive(Debug, PartialEq)] -pub enum Property<E: Extension> { - /// 15.1. creationdate Property - /// - /// Name: creationdate - /// - /// Purpose: Records the time and date the resource was created. - /// - /// Value: date-time (defined in [RFC3339], see the ABNF in Section - /// 5.6.) - /// - /// Protected: MAY be protected. Some servers allow DAV:creationdate - /// to be changed to reflect the time the document was created if that - /// is more meaningful to the user (rather than the time it was - /// uploaded). Thus, clients SHOULD NOT use this property in - /// synchronization logic (use DAV:getetag instead). - /// - /// COPY/MOVE behavior: This property value SHOULD be kept during a - /// MOVE operation, but is normally re-initialized when a resource is - /// created with a COPY. It should not be set in a COPY. - /// - /// Description: The DAV:creationdate property SHOULD be defined on all - /// DAV compliant resources. If present, it contains a timestamp of - /// the moment when the resource was created. Servers that are - /// incapable of persistently recording the creation date SHOULD - /// instead leave it undefined (i.e. report "Not Found"). - /// - /// <!ELEMENT creationdate (#PCDATA) > - CreationDate(DateTime<FixedOffset>), - - /// 15.2. displayname Property - /// - /// Name: displayname - /// - /// Purpose: Provides a name for the resource that is suitable for - /// presentation to a user. - /// - /// Value: Any text. - /// - /// Protected: SHOULD NOT be protected. Note that servers implementing - /// [RFC2518] might have made this a protected property as this is a - /// new requirement. - /// - /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY - /// and MOVE operations. - /// - /// Description: Contains a description of the resource that is - /// suitable for presentation to a user. This property is defined on - /// the resource, and hence SHOULD have the same value independent of - /// the Request-URI used to retrieve it (thus, computing this property - /// based on the Request-URI is deprecated). While generic clients - /// might display the property value to end users, client UI designers - /// must understand that the method for identifying resources is still - /// the URL. Changes to DAV:displayname do not issue moves or copies - /// to the server, but simply change a piece of meta-data on the - /// individual resource. Two resources can have the same DAV: - /// displayname value even within the same collection. - /// - /// <!ELEMENT displayname (#PCDATA) > - DisplayName(String), - - - /// 15.3. getcontentlanguage Property - /// - /// Name: getcontentlanguage - /// - /// Purpose: Contains the Content-Language header value (from Section - /// 14.12 of [RFC2616]) as it would be returned by a GET without - /// accept headers. - /// - /// Value: language-tag (language-tag is defined in Section 3.10 of - /// [RFC2616]) - /// - /// Protected: SHOULD NOT be protected, so that clients can reset the - /// language. Note that servers implementing [RFC2518] might have - /// made this a protected property as this is a new requirement. - /// - /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY - /// and MOVE operations. - /// - /// Description: The DAV:getcontentlanguage property MUST be defined on - /// any DAV-compliant resource that returns the Content-Language - /// header on a GET. - /// - /// <!ELEMENT getcontentlanguage (#PCDATA) > - GetContentLanguage(String), - - /// 15.4. getcontentlength Property - /// - /// Name: getcontentlength - /// - /// Purpose: Contains the Content-Length header returned by a GET - /// without accept headers. - /// - /// Value: See Section 14.13 of [RFC2616]. - /// - /// Protected: This property is computed, therefore protected. - /// - /// Description: The DAV:getcontentlength property MUST be defined on - /// any DAV-compliant resource that returns the Content-Length header - /// in response to a GET. - /// - /// COPY/MOVE behavior: This property value is dependent on the size of - /// the destination resource, not the value of the property on the - /// source resource. - /// - /// <!ELEMENT getcontentlength (#PCDATA) > - GetContentLength(u64), - - /// 15.5. getcontenttype Property - /// - /// Name: getcontenttype - /// - /// Purpose: Contains the Content-Type header value (from Section 14.17 - /// of [RFC2616]) as it would be returned by a GET without accept - /// headers. - /// - /// Value: media-type (defined in Section 3.7 of [RFC2616]) - /// - /// Protected: Potentially protected if the server prefers to assign - /// content types on its own (see also discussion in Section 9.7.1). - /// - /// COPY/MOVE behavior: This property value SHOULD be preserved in COPY - /// and MOVE operations. - /// - /// Description: This property MUST be defined on any DAV-compliant - /// resource that returns the Content-Type header in response to a - /// GET. - /// - /// <!ELEMENT getcontenttype (#PCDATA) > - GetContentType(String), - - /// 15.6. getetag Property - /// - /// Name: getetag - /// - /// Purpose: Contains the ETag header value (from Section 14.19 of - /// [RFC2616]) as it would be returned by a GET without accept - /// headers. - /// - /// Value: entity-tag (defined in Section 3.11 of [RFC2616]) - /// - /// Protected: MUST be protected because this value is created and - /// controlled by the server. - /// - /// COPY/MOVE behavior: This property value is dependent on the final - /// state of the destination resource, not the value of the property - /// on the source resource. Also note the considerations in - /// Section 8.8. - /// - /// Description: The getetag property MUST be defined on any DAV- - /// compliant resource that returns the Etag header. Refer to Section - /// 3.11 of RFC 2616 for a complete definition of the semantics of an - /// ETag, and to Section 8.6 for a discussion of ETags in WebDAV. - /// - /// <!ELEMENT getetag (#PCDATA) > - GetEtag(String), - - /// 15.7. getlastmodified Property - /// - /// Name: getlastmodified - /// - /// Purpose: Contains the Last-Modified header value (from Section - /// 14.29 of [RFC2616]) as it would be returned by a GET method - /// without accept headers. - /// - /// Value: rfc1123-date (defined in Section 3.3.1 of [RFC2616]) - /// - /// Protected: SHOULD be protected because some clients may rely on the - /// value for appropriate caching behavior, or on the value of the - /// Last-Modified header to which this property is linked. - /// - /// COPY/MOVE behavior: This property value is dependent on the last - /// modified date of the destination resource, not the value of the - /// property on the source resource. Note that some server - /// implementations use the file system date modified value for the - /// DAV:getlastmodified value, and this can be preserved in a MOVE - /// even when the HTTP Last-Modified value SHOULD change. Note that - /// since [RFC2616] requires clients to use ETags where provided, a - /// server implementing ETags can count on clients using a much better - /// mechanism than modification dates for offline synchronization or - /// cache control. Also note the considerations in Section 8.8. - /// - /// Description: The last-modified date on a resource SHOULD only - /// reflect changes in the body (the GET responses) of the resource. - /// A change in a property only SHOULD NOT cause the last-modified - /// date to change, because clients MAY rely on the last-modified date - /// to know when to overwrite the existing body. The DAV: - /// getlastmodified property MUST be defined on any DAV-compliant - /// resource that returns the Last-Modified header in response to a - /// GET. - /// - /// <!ELEMENT getlastmodified (#PCDATA) > - GetLastModified(DateTime<FixedOffset>), - - /// 15.8. lockdiscovery Property - /// - /// Name: lockdiscovery - /// - /// Purpose: Describes the active locks on a resource - /// - /// Protected: MUST be protected. Clients change the list of locks - /// through LOCK and UNLOCK, not through PROPPATCH. - /// - /// COPY/MOVE behavior: The value of this property depends on the lock - /// state of the destination, not on the locks of the source resource. - /// Recall that locks are not moved in a MOVE operation. - /// - /// Description: Returns a listing of who has a lock, what type of lock - /// he has, the timeout type and the time remaining on the timeout, - /// and the associated lock token. Owner information MAY be omitted - /// if it is considered sensitive. If there are no locks, but the - /// server supports locks, the property will be present but contain - /// zero 'activelock' elements. If there are one or more locks, an - /// 'activelock' element appears for each lock on the resource. This - /// property is NOT lockable with respect to write locks (Section 7). - /// - /// <!ELEMENT lockdiscovery (activelock)* > - LockDiscovery(Vec<ActiveLock>), - - - /// 15.9. resourcetype Property - /// - /// Name: resourcetype - /// - /// Purpose: Specifies the nature of the resource. - /// - /// Protected: SHOULD be protected. Resource type is generally decided - /// through the operation creating the resource (MKCOL vs PUT), not by - /// PROPPATCH. - /// - /// COPY/MOVE behavior: Generally a COPY/MOVE of a resource results in - /// the same type of resource at the destination. - /// - /// Description: MUST be defined on all DAV-compliant resources. Each - /// child element identifies a specific type the resource belongs to, - /// such as 'collection', which is the only resource type defined by - /// this specification (see Section 14.3). If the element contains - /// the 'collection' child element plus additional unrecognized - /// elements, it should generally be treated as a collection. If the - /// element contains no recognized child elements, it should be - /// treated as a non-collection resource. The default value is empty. - /// This element MUST NOT contain text or mixed content. Any custom - /// child element is considered to be an identifier for a resource - /// type. - /// - /// Example: (fictional example to show extensibility) - /// - /// <x:resourcetype xmlns:x="DAV:"> - /// <x:collection/> - /// <f:search-results xmlns:f="http://www.example.com/ns"/> - /// </x:resourcetype> - ResourceType(Vec<ResourceType<E>>), - - /// 15.10. supportedlock Property - /// - /// Name: supportedlock - /// - /// Purpose: To provide a listing of the lock capabilities supported by - /// the resource. - /// - /// Protected: MUST be protected. Servers, not clients, determine what - /// lock mechanisms are supported. - /// COPY/MOVE behavior: This property value is dependent on the kind of - /// locks supported at the destination, not on the value of the - /// property at the source resource. Servers attempting to COPY to a - /// destination should not attempt to set this property at the - /// destination. - /// - /// Description: Returns a listing of the combinations of scope and - /// access types that may be specified in a lock request on the - /// resource. Note that the actual contents are themselves controlled - /// by access controls, so a server is not required to provide - /// information the client is not authorized to see. This property is - /// NOT lockable with respect to write locks (Section 7). - /// - /// <!ELEMENT supportedlock (lockentry)* > - SupportedLock(Vec<LockEntry>), - - /// Any extension - Extension(E::Property), -} - -#[derive(Debug, PartialEq)] -pub enum ResourceType<E: Extension> { - Collection, - Extension(E::ResourceType), -} diff --git a/src/dav/versioningtypes.rs b/src/dav/versioningtypes.rs deleted file mode 100644 index 6c1c204..0000000 --- a/src/dav/versioningtypes.rs +++ /dev/null @@ -1,3 +0,0 @@ -//@FIXME required for a full DAV implementation -// See section 7.1 of the CalDAV RFC -// It seems it's mainly due to the fact that the REPORT method is re-used. diff --git a/src/dav/xml.rs b/src/dav/xml.rs deleted file mode 100644 index 02263fd..0000000 --- a/src/dav/xml.rs +++ /dev/null @@ -1,273 +0,0 @@ -use tokio::io::{AsyncWrite, AsyncBufRead}; -use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText}; -use quick_xml::name::{Namespace, QName, PrefixDeclaration, ResolveResult, ResolveResult::*}; -use quick_xml::reader::NsReader; - -use super::error::ParsingError; - -// Constants -pub const DAV_URN: &[u8] = b"DAV:"; -pub const CAL_URN: &[u8] = b"urn:ietf:params:xml:ns:caldav"; -pub const CARD_URN: &[u8] = b"urn:ietf:params:xml:ns:carddav"; - -// Async traits -pub trait IWrite = AsyncWrite + Unpin; -pub trait IRead = AsyncBufRead + Unpin; - -// Serialization/Deserialization traits -pub trait QWrite { - async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), quick_xml::Error>; -} -pub trait QRead<T> { - async fn qread(xml: &mut Reader<impl IRead>) -> Result<T, ParsingError>; -} - -// The representation of an XML node in Rust -pub trait Node<T> = QRead<T> + QWrite + std::fmt::Debug + PartialEq; - -// --------------- - -/// Transform a Rust object into an XML stream of characters -pub struct Writer<T: IWrite> { - pub q: quick_xml::writer::Writer<T>, - pub ns_to_apply: Vec<(String, String)>, -} -impl<T: IWrite> Writer<T> { - pub fn create_dav_element(&mut self, name: &str) -> BytesStart<'static> { - self.create_ns_element("D", name) - } - pub fn create_cal_element(&mut self, name: &str) -> BytesStart<'static> { - self.create_ns_element("C", name) - } - - fn create_ns_element(&mut self, ns: &str, name: &str) -> BytesStart<'static> { - let mut start = BytesStart::new(format!("{}:{}", ns, name)); - if !self.ns_to_apply.is_empty() { - start.extend_attributes(self.ns_to_apply.iter().map(|(k, n)| (k.as_str(), n.as_str()))); - self.ns_to_apply.clear() - } - start - } -} - -/// Transform an XML stream of characters into a Rust object -pub struct Reader<T: IRead> { - pub rdr: NsReader<T>, - cur: Event<'static>, - parents: Vec<Event<'static>>, - buf: Vec<u8>, -} -impl<T: IRead> Reader<T> { - pub async fn new(mut rdr: NsReader<T>) -> Result<Self, ParsingError> { - let mut buf: Vec<u8> = vec![]; - let cur = rdr.read_event_into_async(&mut buf).await?.into_owned(); - let parents = vec![]; - buf.clear(); - Ok(Self { cur, parents, rdr, buf }) - } - - /// read one more tag - /// do not expose it publicly - async fn next(&mut self) -> Result<Event<'static>, ParsingError> { - let evt = self.rdr.read_event_into_async(&mut self.buf).await?.into_owned(); - self.buf.clear(); - let old_evt = std::mem::replace(&mut self.cur, evt); - Ok(old_evt) - } - - /// skip a node at current level - /// I would like to make this one private but not ready - pub async fn skip(&mut self) -> Result<Event<'static>, ParsingError> { - //println!("skipping inside node {:?}", self.parents.last()); - match &self.cur { - Event::Start(b) => { - let _span = self.rdr.read_to_end_into_async(b.to_end().name(), &mut self.buf).await?; - self.next().await - }, - Event::End(_) => Err(ParsingError::WrongToken), - Event::Eof => Err(ParsingError::Eof), - _ => self.next().await, - } - } - - /// check if this is the desired tag - fn is_tag(&self, ns: &[u8], key: &str) -> bool { - let qname = match self.peek() { - Event::Start(bs) | Event::Empty(bs) => bs.name(), - Event::End(be) => be.name(), - _ => return false, - }; - - let (extr_ns, local) = self.rdr.resolve_element(qname); - - if local.into_inner() != key.as_bytes() { - return false - } - - match extr_ns { - ResolveResult::Bound(v) => v.into_inner() == ns, - _ => false, - } - } - - fn parent_has_child(&self) -> bool { - matches!(self.parents.last(), Some(Event::Start(_)) | None) - } - - fn ensure_parent_has_child(&self) -> Result<(), ParsingError> { - match self.parent_has_child() { - true => Ok(()), - false => Err(ParsingError::Recoverable), - } - } - - pub fn peek(&self) -> &Event<'static> { - &self.cur - } - - // NEW API - pub async fn tag_string(&mut self) -> Result<String, ParsingError> { - self.ensure_parent_has_child()?; - - let mut acc = String::new(); - loop { - match self.peek() { - Event::CData(unescaped) => { - acc.push_str(std::str::from_utf8(unescaped.as_ref())?); - self.next().await? - }, - Event::Text(escaped) => { - acc.push_str(escaped.unescape()?.as_ref()); - self.next().await? - } - Event::End(_) | Event::Start(_) | Event::Empty(_) => return Ok(acc), - _ => self.next().await?, - }; - } - } - - pub async fn maybe_read<N: Node<N>>(&mut self, t: &mut Option<N>, dirty: &mut bool) -> Result<(), ParsingError> { - if !self.parent_has_child() { - return Ok(()) - } - - match N::qread(self).await { - Ok(v) => { - *t = Some(v); - *dirty = true; - Ok(()) - }, - Err(ParsingError::Recoverable) => Ok(()), - Err(e) => Err(e), - } - } - - pub async fn maybe_push<N: Node<N>>(&mut self, t: &mut Vec<N>, dirty: &mut bool) -> Result<(), ParsingError> { - if !self.parent_has_child() { - return Ok(()) - } - - match N::qread(self).await { - Ok(v) => { - t.push(v); - *dirty = true; - Ok(()) - }, - Err(ParsingError::Recoverable) => Ok(()), - Err(e) => Err(e), - } - } - - pub async fn find<N: Node<N>>(&mut self) -> Result<N, ParsingError> { - self.ensure_parent_has_child()?; - - loop { - // Try parse - match N::qread(self).await { - Err(ParsingError::Recoverable) => (), - otherwise => return otherwise, - } - - // If recovered, skip the element - self.skip().await?; - } - } - - pub async fn maybe_find<N: Node<N>>(&mut self) -> Result<Option<N>, ParsingError> { - self.ensure_parent_has_child()?; - - loop { - // Try parse - match N::qread(self).await { - Err(ParsingError::Recoverable) => (), - otherwise => return otherwise.map(Some), - } - - match self.peek() { - Event::End(_) => return Ok(None), - _ => self.skip().await?, - }; - } - } - - pub async fn collect<N: Node<N>>(&mut self) -> Result<Vec<N>, ParsingError> { - self.ensure_parent_has_child()?; - let mut acc = Vec::new(); - - loop { - match N::qread(self).await { - Err(ParsingError::Recoverable) => match self.peek() { - Event::End(_) => return Ok(acc), - _ => { - self.skip().await?; - }, - }, - Ok(v) => acc.push(v), - Err(e) => return Err(e), - } - } - } - - pub async fn open(&mut self, ns: &[u8], key: &str) -> Result<Event<'static>, ParsingError> { - let evt = match self.peek() { - Event::Empty(_) if self.is_tag(ns, key) => self.cur.clone(), - Event::Start(_) if self.is_tag(ns, key) => self.next().await?, - _ => return Err(ParsingError::Recoverable), - }; - - //println!("open tag {:?}", evt); - self.parents.push(evt.clone()); - Ok(evt) - } - - pub async fn maybe_open(&mut self, ns: &[u8], key: &str) -> Result<Option<Event<'static>>, ParsingError> { - match self.open(ns, key).await { - Ok(v) => Ok(Some(v)), - Err(ParsingError::Recoverable) => Ok(None), - Err(e) => Err(e), - } - } - - // find stop tag - pub async fn close(&mut self) -> Result<Event<'static>, ParsingError> { - //println!("close tag {:?}", self.parents.last()); - - // Handle the empty case - if !self.parent_has_child() { - self.parents.pop(); - return self.next().await - } - - // Handle the start/end case - loop { - match self.peek() { - Event::End(_) => { - self.parents.pop(); - return self.next().await - }, - _ => self.skip().await?, - }; - } - } -} - diff --git a/src/imap/attributes.rs b/src/imap/attributes.rs deleted file mode 100644 index 89446a8..0000000 --- a/src/imap/attributes.rs +++ /dev/null @@ -1,77 +0,0 @@ -use imap_codec::imap_types::command::FetchModifier; -use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section}; - -/// Internal decisions based on fetched attributes -/// passed by the client - -pub struct AttributesProxy { - pub attrs: Vec<MessageDataItemName<'static>>, -} -impl AttributesProxy { - pub fn new( - attrs: &MacroOrMessageDataItemNames<'static>, - modifiers: &[FetchModifier], - is_uid_fetch: bool, - ) -> Self { - // Expand macros - let mut fetch_attrs = match attrs { - MacroOrMessageDataItemNames::Macro(m) => { - use imap_codec::imap_types::fetch::Macro; - use MessageDataItemName::*; - match m { - Macro::All => vec![Flags, InternalDate, Rfc822Size, Envelope], - Macro::Fast => vec![Flags, InternalDate, Rfc822Size], - Macro::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body], - _ => { - tracing::error!("unimplemented macro"); - vec![] - } - } - } - MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(), - }; - - // Handle uids - if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) { - fetch_attrs.push(MessageDataItemName::Uid); - } - - // Handle inferred MODSEQ tag - let is_changed_since = modifiers - .iter() - .any(|m| matches!(m, FetchModifier::ChangedSince(..))); - if is_changed_since && !fetch_attrs.contains(&MessageDataItemName::ModSeq) { - fetch_attrs.push(MessageDataItemName::ModSeq); - } - - Self { attrs: fetch_attrs } - } - - pub fn is_enabling_condstore(&self) -> bool { - self.attrs - .iter() - .any(|x| matches!(x, MessageDataItemName::ModSeq)) - } - - pub fn need_body(&self) -> bool { - self.attrs.iter().any(|x| match x { - MessageDataItemName::Body - | MessageDataItemName::Rfc822 - | MessageDataItemName::Rfc822Text - | MessageDataItemName::BodyStructure => true, - - MessageDataItemName::BodyExt { - section: Some(section), - partial: _, - peek: _, - } => match section { - Section::Header(None) - | Section::HeaderFields(None, _) - | Section::HeaderFieldsNot(None, _) => false, - _ => true, - }, - MessageDataItemName::BodyExt { .. } => true, - _ => false, - }) - } -} diff --git a/src/imap/capability.rs b/src/imap/capability.rs deleted file mode 100644 index c76b51c..0000000 --- a/src/imap/capability.rs +++ /dev/null @@ -1,159 +0,0 @@ -use imap_codec::imap_types::command::{FetchModifier, SelectExamineModifier, StoreModifier}; -use imap_codec::imap_types::core::Vec1; -use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; -use imap_codec::imap_types::response::Capability; -use std::collections::HashSet; - -use crate::imap::attributes::AttributesProxy; - -fn capability_unselect() -> Capability<'static> { - Capability::try_from("UNSELECT").unwrap() -} - -fn capability_condstore() -> Capability<'static> { - Capability::try_from("CONDSTORE").unwrap() -} - -fn capability_uidplus() -> Capability<'static> { - Capability::try_from("UIDPLUS").unwrap() -} - -fn capability_liststatus() -> Capability<'static> { - Capability::try_from("LIST-STATUS").unwrap() -} - -/* -fn capability_qresync() -> Capability<'static> { - Capability::try_from("QRESYNC").unwrap() -} -*/ - -#[derive(Debug, Clone)] -pub struct ServerCapability(HashSet<Capability<'static>>); - -impl Default for ServerCapability { - fn default() -> Self { - Self(HashSet::from([ - Capability::Imap4Rev1, - Capability::Enable, - Capability::Move, - Capability::LiteralPlus, - Capability::Idle, - capability_unselect(), - capability_condstore(), - capability_uidplus(), - capability_liststatus(), - //capability_qresync(), - ])) - } -} - -impl ServerCapability { - pub fn to_vec(&self) -> Vec1<Capability<'static>> { - self.0 - .iter() - .map(|v| v.clone()) - .collect::<Vec<_>>() - .try_into() - .unwrap() - } - - #[allow(dead_code)] - pub fn support(&self, cap: &Capability<'static>) -> bool { - self.0.contains(cap) - } -} - -#[derive(Clone)] -pub enum ClientStatus { - NotSupportedByServer, - Disabled, - Enabled, -} -impl ClientStatus { - pub fn is_enabled(&self) -> bool { - matches!(self, Self::Enabled) - } - - pub fn enable(&self) -> Self { - match self { - Self::Disabled => Self::Enabled, - other => other.clone(), - } - } -} - -pub struct ClientCapability { - pub condstore: ClientStatus, - pub utf8kind: Option<Utf8Kind>, -} - -impl ClientCapability { - pub fn new(sc: &ServerCapability) -> Self { - Self { - condstore: match sc.0.contains(&capability_condstore()) { - true => ClientStatus::Disabled, - _ => ClientStatus::NotSupportedByServer, - }, - utf8kind: None, - } - } - - pub fn enable_condstore(&mut self) { - self.condstore = self.condstore.enable(); - } - - pub fn attributes_enable(&mut self, ap: &AttributesProxy) { - if ap.is_enabling_condstore() { - self.enable_condstore() - } - } - - pub fn fetch_modifiers_enable(&mut self, mods: &[FetchModifier]) { - if mods - .iter() - .any(|x| matches!(x, FetchModifier::ChangedSince(..))) - { - self.enable_condstore() - } - } - - pub fn store_modifiers_enable(&mut self, mods: &[StoreModifier]) { - if mods - .iter() - .any(|x| matches!(x, StoreModifier::UnchangedSince(..))) - { - self.enable_condstore() - } - } - - pub fn select_enable(&mut self, mods: &[SelectExamineModifier]) { - for m in mods.iter() { - match m { - SelectExamineModifier::Condstore => self.enable_condstore(), - } - } - } - - pub fn try_enable( - &mut self, - caps: &[CapabilityEnable<'static>], - ) -> Vec<CapabilityEnable<'static>> { - let mut enabled = vec![]; - for cap in caps { - match cap { - CapabilityEnable::CondStore if matches!(self.condstore, ClientStatus::Disabled) => { - self.condstore = ClientStatus::Enabled; - enabled.push(cap.clone()); - } - CapabilityEnable::Utf8(kind) if Some(kind) != self.utf8kind.as_ref() => { - self.utf8kind = Some(kind.clone()); - enabled.push(cap.clone()); - } - _ => (), - } - } - - enabled - } -} diff --git a/src/imap/command/anonymous.rs b/src/imap/command/anonymous.rs deleted file mode 100644 index 811d1e4..0000000 --- a/src/imap/command/anonymous.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::AString; -use imap_codec::imap_types::response::Code; -use imap_codec::imap_types::secret::Secret; - -use crate::imap::capability::ServerCapability; -use crate::imap::command::anystate; -use crate::imap::flow; -use crate::imap::response::Response; -use crate::login::ArcLoginProvider; -use crate::user::User; - -//--- dispatching - -pub struct AnonymousContext<'a> { - pub req: &'a Command<'static>, - pub server_capabilities: &'a ServerCapability, - pub login_provider: &'a ArcLoginProvider, -} - -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any State - CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to anonymous context (3 commands) - CommandBody::Login { username, password } => ctx.login(username, password).await, - CommandBody::Authenticate { .. } => { - anystate::not_implemented(ctx.req.tag.clone(), "authenticate") - } - //StartTLS is not implemented for now, we will probably go full TLS. - - // Collect other commands - _ => anystate::wrong_state(ctx.req.tag.clone()), - } -} - -//--- Command controllers, private - -impl<'a> AnonymousContext<'a> { - async fn login( - self, - username: &AString<'a>, - password: &Secret<AString<'a>>, - ) -> Result<(Response<'static>, flow::Transition)> { - let (u, p) = ( - std::str::from_utf8(username.as_ref())?, - std::str::from_utf8(password.declassify().as_ref())?, - ); - tracing::info!(user = %u, "command.login"); - - let creds = match self.login_provider.login(&u, &p).await { - Err(e) => { - tracing::debug!(error=%e, "authentication failed"); - return Ok(( - Response::build() - .to_req(self.req) - .message("Authentication failed") - .no()?, - flow::Transition::None, - )); - } - Ok(c) => c, - }; - - let user = User::new(u.to_string(), creds).await?; - - tracing::info!(username=%u, "connected"); - Ok(( - Response::build() - .to_req(self.req) - .code(Code::Capability(self.server_capabilities.to_vec())) - .message("Completed") - .ok()?, - flow::Transition::Authenticate(user), - )) - } -} diff --git a/src/imap/command/anystate.rs b/src/imap/command/anystate.rs deleted file mode 100644 index 718ba3f..0000000 --- a/src/imap/command/anystate.rs +++ /dev/null @@ -1,54 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::core::Tag; -use imap_codec::imap_types::response::Data; - -use crate::imap::capability::ServerCapability; -use crate::imap::flow; -use crate::imap::response::Response; - -pub(crate) fn capability( - tag: Tag<'static>, - cap: &ServerCapability, -) -> Result<(Response<'static>, flow::Transition)> { - let res = Response::build() - .tag(tag) - .message("Server capabilities") - .data(Data::Capability(cap.to_vec())) - .ok()?; - - Ok((res, flow::Transition::None)) -} - -pub(crate) fn noop_nothing(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build().tag(tag).message("Noop completed.").ok()?, - flow::Transition::None, - )) -} - -pub(crate) fn logout() -> Result<(Response<'static>, flow::Transition)> { - Ok((Response::bye()?, flow::Transition::Logout)) -} - -pub(crate) fn not_implemented<'a>( - tag: Tag<'a>, - what: &str, -) -> Result<(Response<'a>, flow::Transition)> { - Ok(( - Response::build() - .tag(tag) - .message(format!("Command not implemented {}", what)) - .bad()?, - flow::Transition::None, - )) -} - -pub(crate) fn wrong_state(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .tag(tag) - .message("Command not authorized in this state") - .bad()?, - flow::Transition::None, - )) -} diff --git a/src/imap/command/authenticated.rs b/src/imap/command/authenticated.rs deleted file mode 100644 index 3d332ec..0000000 --- a/src/imap/command/authenticated.rs +++ /dev/null @@ -1,683 +0,0 @@ -use std::collections::BTreeMap; -use std::sync::Arc; -use thiserror::Error; - -use anyhow::{anyhow, bail, Result}; -use imap_codec::imap_types::command::{ - Command, CommandBody, ListReturnItem, SelectExamineModifier, -}; -use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, Vec1}; -use imap_codec::imap_types::datetime::DateTime; -use imap_codec::imap_types::extensions::enable::CapabilityEnable; -use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; -use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; -use imap_codec::imap_types::response::{Code, CodeOther, Data}; -use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; - -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anystate, MailboxName}; -use crate::imap::flow; -use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; -use crate::imap::response::Response; -use crate::imap::Body; - -use crate::mail::uidindex::*; -use crate::user::User; -use crate::mail::IMF; -use crate::mail::namespace::MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW; - -pub struct AuthenticatedContext<'a> { - pub req: &'a Command<'static>, - pub server_capabilities: &'a ServerCapability, - pub client_capabilities: &'a mut ClientCapability, - pub user: &'a Arc<User>, -} - -pub async fn dispatch<'a>( - mut ctx: AuthenticatedContext<'a>, -) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any state - CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to this state (11 commands) - CommandBody::Create { mailbox } => ctx.create(mailbox).await, - CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, - CommandBody::Rename { from, to } => ctx.rename(from, to).await, - CommandBody::Lsub { - reference, - mailbox_wildcard, - } => ctx.list(reference, mailbox_wildcard, &[], true).await, - CommandBody::List { - reference, - mailbox_wildcard, - r#return, - } => ctx.list(reference, mailbox_wildcard, r#return, false).await, - CommandBody::Status { - mailbox, - item_names, - } => ctx.status(mailbox, item_names).await, - CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, - CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, - CommandBody::Select { mailbox, modifiers } => ctx.select(mailbox, modifiers).await, - CommandBody::Examine { mailbox, modifiers } => ctx.examine(mailbox, modifiers).await, - CommandBody::Append { - mailbox, - flags, - date, - message, - } => ctx.append(mailbox, flags, date, message).await, - - // rfc5161 ENABLE - CommandBody::Enable { capabilities } => ctx.enable(capabilities), - - // Collect other commands - _ => anystate::wrong_state(ctx.req.tag.clone()), - } -} - -// --- PRIVATE --- -impl<'a> AuthenticatedContext<'a> { - async fn create( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name = match mailbox { - MailboxCodec::Inbox => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Cannot create INBOX") - .bad()?, - flow::Transition::None, - )); - } - MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?, - }; - - match self.user.create_mailbox(&name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("CREATE complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(&e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn delete( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - match self.user.delete_mailbox(&name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("DELETE complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn rename( - self, - from: &MailboxCodec<'a>, - to: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(from).try_into()?; - let new_name: &str = MailboxName(to).try_into()?; - - match self.user.rename_mailbox(&name, &new_name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("RENAME complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn list( - &mut self, - reference: &MailboxCodec<'a>, - mailbox_wildcard: &ListMailbox<'a>, - must_return: &[ListReturnItem], - is_lsub: bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); - - let reference: &str = MailboxName(reference).try_into()?; - if !reference.is_empty() { - return Ok(( - Response::build() - .to_req(self.req) - .message("References not supported") - .bad()?, - flow::Transition::None, - )); - } - - let status_item_names = must_return.iter().find_map(|m| match m { - ListReturnItem::Status(v) => Some(v), - _ => None, - }); - - // @FIXME would probably need a rewrite to better use the imap_codec library - let wildcard = match mailbox_wildcard { - ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?, - ListMailbox::String(v) => std::str::from_utf8(v.as_ref())?, - }; - if wildcard.is_empty() { - if is_lsub { - return Ok(( - Response::build() - .to_req(self.req) - .message("LSUB complete") - .data(Data::Lsub { - items: vec![], - delimiter: Some(mbx_hier_delim), - mailbox: "".try_into().unwrap(), - }) - .ok()?, - flow::Transition::None, - )); - } else { - return Ok(( - Response::build() - .to_req(self.req) - .message("LIST complete") - .data(Data::List { - items: vec![], - delimiter: Some(mbx_hier_delim), - mailbox: "".try_into().unwrap(), - }) - .ok()?, - flow::Transition::None, - )); - } - } - - let mailboxes = self.user.list_mailboxes().await?; - let mut vmailboxes = BTreeMap::new(); - for mb in mailboxes.iter() { - for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) { - if i > 0 { - let smb = &mb[..i]; - vmailboxes.entry(smb).or_insert(false); - } - } - vmailboxes.insert(mb, true); - } - - let mut ret = vec![]; - for (mb, is_real) in vmailboxes.iter() { - if matches_wildcard(&wildcard, mb) { - let mailbox: MailboxCodec = mb - .to_string() - .try_into() - .map_err(|_| anyhow!("invalid mailbox name"))?; - let mut items = vec![FlagNameAttribute::from(Atom::unvalidated("Subscribed"))]; - - // Decoration - if !*is_real { - items.push(FlagNameAttribute::Noselect); - } else { - match *mb { - "Drafts" => items.push(Atom::unvalidated("Drafts").into()), - "Archive" => items.push(Atom::unvalidated("Archive").into()), - "Sent" => items.push(Atom::unvalidated("Sent").into()), - "Trash" => items.push(Atom::unvalidated("Trash").into()), - _ => (), - }; - } - - // Result type - if is_lsub { - ret.push(Data::Lsub { - items, - delimiter: Some(mbx_hier_delim), - mailbox: mailbox.clone(), - }); - } else { - ret.push(Data::List { - items, - delimiter: Some(mbx_hier_delim), - mailbox: mailbox.clone(), - }); - } - - // Also collect status - if let Some(sin) = status_item_names { - let ret_attrs = match self.status_items(mb, sin).await { - Ok(a) => a, - Err(e) => { - tracing::error!(err=?e, mailbox=%mb, "Unable to fetch status for mailbox"); - continue; - } - }; - - let data = Data::Status { - mailbox, - items: ret_attrs.into(), - }; - - ret.push(data); - } - } - } - - let msg = if is_lsub { - "LSUB completed" - } else { - "LIST completed" - }; - Ok(( - Response::build() - .to_req(self.req) - .message(msg) - .many_data(ret) - .ok()?, - flow::Transition::None, - )) - } - - async fn status( - &mut self, - mailbox: &MailboxCodec<'static>, - attributes: &[StatusDataItemName], - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - let ret_attrs = match self.status_items(name, attributes).await { - Ok(v) => v, - Err(e) => match e.downcast_ref::<CommandError>() { - Some(CommandError::MailboxNotFound) => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - _ => return Err(e.into()), - }, - }; - - let data = Data::Status { - mailbox: mailbox.clone(), - items: ret_attrs.into(), - }; - - Ok(( - Response::build() - .to_req(self.req) - .message("STATUS completed") - .data(data) - .ok()?, - flow::Transition::None, - )) - } - - async fn status_items( - &mut self, - name: &str, - attributes: &[StatusDataItemName], - ) -> Result<Vec<StatusDataItem>> { - let mb_opt = self.user.open_mailbox(name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => return Err(CommandError::MailboxNotFound.into()), - }; - - let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - - let mut ret_attrs = vec![]; - for attr in attributes.iter() { - ret_attrs.push(match attr { - StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?), - StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32), - StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?), - StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()), - StatusDataItemName::UidValidity => { - StatusDataItem::UidValidity(view.uidvalidity()) - } - StatusDataItemName::Deleted => { - bail!("quota not implemented, can't return deleted elements waiting for EXPUNGE"); - }, - StatusDataItemName::DeletedStorage => { - bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); - }, - StatusDataItemName::HighestModSeq => { - self.client_capabilities.enable_condstore(); - StatusDataItem::HighestModSeq(view.highestmodseq().get()) - }, - }); - } - Ok(ret_attrs) - } - - async fn subscribe( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - if self.user.has_mailbox(&name).await? { - Ok(( - Response::build() - .to_req(self.req) - .message("SUBSCRIBE complete") - .ok()?, - flow::Transition::None, - )) - } else { - Ok(( - Response::build() - .to_req(self.req) - .message(format!("Mailbox {} does not exist", name)) - .bad()?, - flow::Transition::None, - )) - } - } - - async fn unsubscribe( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - if self.user.has_mailbox(&name).await? { - Ok(( - Response::build() - .to_req(self.req) - .message(format!( - "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", - name - )) - .bad()?, - flow::Transition::None, - )) - } else { - Ok(( - Response::build() - .to_req(self.req) - .message(format!("Mailbox {} does not exist", name)) - .no()?, - flow::Transition::None, - )) - } - } - - /* - * TRACE BEGIN --- - - - Example: C: A142 SELECT INBOX - S: * 172 EXISTS - S: * 1 RECENT - S: * OK [UNSEEN 12] Message 12 is first unseen - S: * OK [UIDVALIDITY 3857529045] UIDs valid - S: * OK [UIDNEXT 4392] Predicted next UID - S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) - S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited - S: A142 OK [READ-WRITE] SELECT completed - - --- a mailbox with no unseen message -> no unseen entry - NOTES: - RFC3501 (imap4rev1) says if there is no OK [UNSEEN] response, client must make no assumption, - it is therefore correct to not return it even if there are unseen messages - RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE - For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications. - - - 20 select "INBOX.achats" - * FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1) - * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1 \*)] Flags permitted. - * 88 EXISTS - * 0 RECENT - * OK [UIDVALIDITY 1347986788] UIDs valid - * OK [UIDNEXT 91] Predicted next UID - * OK [HIGHESTMODSEQ 72] Highest - 20 OK [READ-WRITE] Select completed (0.001 + 0.000 secs). - - * TRACE END --- - */ - async fn select( - self, - mailbox: &MailboxCodec<'a>, - modifiers: &[SelectExamineModifier], - ) -> Result<(Response<'static>, flow::Transition)> { - self.client_capabilities.select_enable(modifiers); - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - }; - tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); - - let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - let data = mb.summary()?; - - Ok(( - Response::build() - .message("Select completed") - .to_req(self.req) - .code(Code::ReadWrite) - .set_body(data) - .ok()?, - flow::Transition::Select(mb, flow::MailboxPerm::ReadWrite), - )) - } - - async fn examine( - self, - mailbox: &MailboxCodec<'a>, - modifiers: &[SelectExamineModifier], - ) -> Result<(Response<'static>, flow::Transition)> { - self.client_capabilities.select_enable(modifiers); - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - }; - tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); - - let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - let data = mb.summary()?; - - Ok(( - Response::build() - .to_req(self.req) - .message("Examine completed") - .code(Code::ReadOnly) - .set_body(data) - .ok()?, - flow::Transition::Select(mb, flow::MailboxPerm::ReadOnly), - )) - } - - //@FIXME we should write a specific version for the "selected" state - //that returns some unsollicited responses - async fn append( - self, - mailbox: &MailboxCodec<'a>, - flags: &[Flag<'a>], - date: &Option<DateTime>, - message: &Literal<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let append_tag = self.req.tag.clone(); - match self.append_internal(mailbox, flags, date, message).await { - Ok((_mb_view, uidvalidity, uid, _modseq)) => Ok(( - Response::build() - .tag(append_tag) - .message("APPEND completed") - .code(Code::Other(CodeOther::unvalidated( - format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), - ))) - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .tag(append_tag) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - fn enable( - self, - cap_enable: &Vec1<CapabilityEnable<'static>>, - ) -> Result<(Response<'static>, flow::Transition)> { - let mut response_builder = Response::build().to_req(self.req); - let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref()); - if capabilities.len() > 0 { - response_builder = response_builder.data(Data::Enabled { capabilities }); - } - Ok(( - response_builder.message("ENABLE completed").ok()?, - flow::Transition::None, - )) - } - - //@FIXME should be refactored and integrated to the mailbox view - pub(crate) async fn append_internal( - self, - mailbox: &MailboxCodec<'a>, - flags: &[Flag<'a>], - date: &Option<DateTime>, - message: &Literal<'a>, - ) -> Result<(MailboxView, ImapUidvalidity, ImapUid, ModSeq)> { - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => bail!("Mailbox does not exist"), - }; - let mut view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - - if date.is_some() { - tracing::warn!("Cannot set date when appending message"); - } - - let msg = - IMF::try_from(message.data()).map_err(|_| anyhow!("Could not parse e-mail message"))?; - let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); - // TODO: filter allowed flags? ping @Quentin - - let (uidvalidity, uid, modseq) = - view.internal.mailbox.append(msg, None, &flags[..]).await?; - //let unsollicited = view.update(UpdateParameters::default()).await?; - - Ok((view, uidvalidity, uid, modseq)) - } -} - -fn matches_wildcard(wildcard: &str, name: &str) -> bool { - let wildcard = wildcard.chars().collect::<Vec<char>>(); - let name = name.chars().collect::<Vec<char>>(); - - let mut matches = vec![vec![false; wildcard.len() + 1]; name.len() + 1]; - - for i in 0..=name.len() { - for j in 0..=wildcard.len() { - matches[i][j] = (i == 0 && j == 0) - || (j > 0 - && matches[i][j - 1] - && (wildcard[j - 1] == '%' || wildcard[j - 1] == '*')) - || (i > 0 - && j > 0 - && matches[i - 1][j - 1] - && wildcard[j - 1] == name[i - 1] - && wildcard[j - 1] != '%' - && wildcard[j - 1] != '*') - || (i > 0 - && j > 0 - && matches[i - 1][j] - && (wildcard[j - 1] == '*' - || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW))); - } - } - - matches[name.len()][wildcard.len()] -} - -#[derive(Error, Debug)] -pub enum CommandError { - #[error("Mailbox not found")] - MailboxNotFound, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_wildcard_matches() { - assert!(matches_wildcard("INBOX", "INBOX")); - assert!(matches_wildcard("*", "INBOX")); - assert!(matches_wildcard("%", "INBOX")); - assert!(!matches_wildcard("%", "Test.Azerty")); - assert!(!matches_wildcard("INBOX.*", "INBOX")); - assert!(matches_wildcard("Sent.*", "Sent.A")); - assert!(matches_wildcard("Sent.*", "Sent.A.B")); - assert!(!matches_wildcard("Sent.%", "Sent.A.B")); - } -} diff --git a/src/imap/command/mod.rs b/src/imap/command/mod.rs deleted file mode 100644 index f201eb6..0000000 --- a/src/imap/command/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod anonymous; -pub mod anystate; -pub mod authenticated; -pub mod selected; - -use crate::mail::namespace::INBOX; -use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; - -/// Convert an IMAP mailbox name/identifier representation -/// to an utf-8 string that is used internally in Aerogramme -struct MailboxName<'a>(&'a MailboxCodec<'a>); -impl<'a> TryInto<&'a str> for MailboxName<'a> { - type Error = std::str::Utf8Error; - fn try_into(self) -> Result<&'a str, Self::Error> { - match self.0 { - MailboxCodec::Inbox => Ok(INBOX), - MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), - } - } -} diff --git a/src/imap/command/selected.rs b/src/imap/command/selected.rs deleted file mode 100644 index eedfbd6..0000000 --- a/src/imap/command/selected.rs +++ /dev/null @@ -1,424 +0,0 @@ -use std::num::NonZeroU64; -use std::sync::Arc; - -use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier}; -use imap_codec::imap_types::core::{Charset, Vec1}; -use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; -use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; -use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; -use imap_codec::imap_types::response::{Code, CodeOther}; -use imap_codec::imap_types::search::SearchKey; -use imap_codec::imap_types::sequence::SequenceSet; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anystate, authenticated, MailboxName}; -use crate::imap::flow; -use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; -use crate::imap::response::Response; -use crate::user::User; - -pub struct SelectedContext<'a> { - pub req: &'a Command<'static>, - pub user: &'a Arc<User>, - pub mailbox: &'a mut MailboxView, - pub server_capabilities: &'a ServerCapability, - pub client_capabilities: &'a mut ClientCapability, - pub perm: &'a flow::MailboxPerm, -} - -pub async fn dispatch<'a>( - ctx: SelectedContext<'a>, -) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any State - // noop is specific to this state - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to this state (7 commands + NOOP) - CommandBody::Close => match ctx.perm { - flow::MailboxPerm::ReadWrite => ctx.close().await, - flow::MailboxPerm::ReadOnly => ctx.examine_close().await, - }, - CommandBody::Noop | CommandBody::Check => ctx.noop().await, - CommandBody::Fetch { - sequence_set, - macro_or_item_names, - modifiers, - uid, - } => { - ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid) - .await - } - //@FIXME SearchKey::And is a legacy hack, should be refactored - CommandBody::Search { - charset, - criteria, - uid, - } => { - ctx.search(charset, &SearchKey::And(criteria.clone()), uid) - .await - } - CommandBody::Expunge { - // UIDPLUS (rfc4315) - uid_sequence_set, - } => ctx.expunge(uid_sequence_set).await, - CommandBody::Store { - sequence_set, - kind, - response, - flags, - modifiers, - uid, - } => { - ctx.store(sequence_set, kind, response, flags, modifiers, uid) - .await - } - CommandBody::Copy { - sequence_set, - mailbox, - uid, - } => ctx.copy(sequence_set, mailbox, uid).await, - CommandBody::Move { - sequence_set, - mailbox, - uid, - } => ctx.r#move(sequence_set, mailbox, uid).await, - - // UNSELECT extension (rfc3691) - CommandBody::Unselect => ctx.unselect().await, - - // In selected mode, we fallback to authenticated when needed - _ => { - authenticated::dispatch(authenticated::AuthenticatedContext { - req: ctx.req, - server_capabilities: ctx.server_capabilities, - client_capabilities: ctx.client_capabilities, - user: ctx.user, - }) - .await - } - } -} - -// --- PRIVATE --- - -impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response<'static>, flow::Transition)> { - // We expunge messages, - // but we don't send the untagged EXPUNGE responses - let tag = self.req.tag.clone(); - self.expunge(&None).await?; - Ok(( - Response::build().tag(tag).message("CLOSE completed").ok()?, - flow::Transition::Unselect, - )) - } - - /// CLOSE in examined state is not the same as in selected state - /// (in selected state it also does an EXPUNGE, here it doesn't) - async fn examine_close(self) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .to_req(self.req) - .message("CLOSE completed") - .ok()?, - flow::Transition::Unselect, - )) - } - - async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .to_req(self.req) - .message("UNSELECT completed") - .ok()?, - flow::Transition::Unselect, - )) - } - - pub async fn fetch( - self, - sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'static>, - modifiers: &[FetchModifier], - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let ap = AttributesProxy::new(attributes, modifiers, *uid); - let mut changed_since: Option<NonZeroU64> = None; - modifiers.iter().for_each(|m| match m { - FetchModifier::ChangedSince(val) => { - changed_since = Some(*val); - } - }); - - match self - .mailbox - .fetch(sequence_set, &ap, changed_since, uid) - .await - { - Ok(resp) => { - // Capabilities enabling logic only on successful command - // (according to my understanding of the spec) - self.client_capabilities.attributes_enable(&ap); - self.client_capabilities.fetch_modifiers_enable(modifiers); - - // Response to the client - Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )) - } - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - pub async fn search( - self, - charset: &Option<Charset<'a>>, - criteria: &SearchKey<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?; - if enable_condstore { - self.client_capabilities.enable_condstore(); - } - Ok(( - Response::build() - .to_req(self.req) - .set_body(found) - .message("SEARCH completed") - .ok()?, - flow::Transition::None, - )) - } - - pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { - self.mailbox.internal.mailbox.force_sync().await?; - - let updates = self.mailbox.update(UpdateParameters::default()).await?; - Ok(( - Response::build() - .to_req(self.req) - .message("NOOP completed.") - .set_body(updates) - .ok()?, - flow::Transition::None, - )) - } - - async fn expunge( - self, - uid_sequence_set: &Option<SequenceSet>, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let tag = self.req.tag.clone(); - let data = self.mailbox.expunge(uid_sequence_set).await?; - - Ok(( - Response::build() - .tag(tag) - .message("EXPUNGE completed") - .set_body(data) - .ok()?, - flow::Transition::None, - )) - } - - async fn store( - self, - sequence_set: &SequenceSet, - kind: &StoreType, - response: &StoreResponse, - flags: &[Flag<'a>], - modifiers: &[StoreModifier], - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let mut unchanged_since: Option<NonZeroU64> = None; - modifiers.iter().for_each(|m| match m { - StoreModifier::UnchangedSince(val) => { - unchanged_since = Some(*val); - } - }); - - let (data, modified) = self - .mailbox - .store(sequence_set, kind, response, flags, unchanged_since, uid) - .await?; - - let mut ok_resp = Response::build() - .to_req(self.req) - .message("STORE completed") - .set_body(data); - - match modified[..] { - [] => (), - [_head, ..] => { - let modified_str = format!( - "MODIFIED {}", - modified - .into_iter() - .map(|x| x.to_string()) - .collect::<Vec<_>>() - .join(",") - ); - ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated( - modified_str.into_bytes(), - ))); - } - }; - - self.client_capabilities.store_modifiers_enable(modifiers); - - Ok((ok_resp.ok()?, flow::Transition::None)) - } - - async fn copy( - self, - sequence_set: &SequenceSet, - mailbox: &MailboxCodec<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - //@FIXME Could copy be valid in EXAMINE mode? - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Destination mailbox does not exist") - .code(Code::TryCreate) - .no()?, - flow::Transition::None, - )) - } - }; - - let (uidval, uid_map) = self.mailbox.copy(sequence_set, mb, uid).await?; - - let copyuid_str = format!( - "{} {} {}", - uidval, - uid_map - .iter() - .map(|(sid, _)| format!("{}", sid)) - .collect::<Vec<_>>() - .join(","), - uid_map - .iter() - .map(|(_, tuid)| format!("{}", tuid)) - .collect::<Vec<_>>() - .join(",") - ); - - Ok(( - Response::build() - .to_req(self.req) - .message("COPY completed") - .code(Code::Other(CodeOther::unvalidated( - format!("COPYUID {}", copyuid_str).into_bytes(), - ))) - .ok()?, - flow::Transition::None, - )) - } - - async fn r#move( - self, - sequence_set: &SequenceSet, - mailbox: &MailboxCodec<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Destination mailbox does not exist") - .code(Code::TryCreate) - .no()?, - flow::Transition::None, - )) - } - }; - - let (uidval, uid_map, data) = self.mailbox.r#move(sequence_set, mb, uid).await?; - - // compute code - let copyuid_str = format!( - "{} {} {}", - uidval, - uid_map - .iter() - .map(|(sid, _)| format!("{}", sid)) - .collect::<Vec<_>>() - .join(","), - uid_map - .iter() - .map(|(_, tuid)| format!("{}", tuid)) - .collect::<Vec<_>>() - .join(",") - ); - - Ok(( - Response::build() - .to_req(self.req) - .message("COPY completed") - .code(Code::Other(CodeOther::unvalidated( - format!("COPYUID {}", copyuid_str).into_bytes(), - ))) - .set_body(data) - .ok()?, - flow::Transition::None, - )) - } - - fn fail_read_only(&self) -> Option<Response<'static>> { - match self.perm { - flow::MailboxPerm::ReadWrite => None, - flow::MailboxPerm::ReadOnly => Some( - Response::build() - .to_req(self.req) - .message("Write command are forbidden while exmining mailbox") - .no() - .unwrap(), - ), - } - } -} diff --git a/src/imap/flags.rs b/src/imap/flags.rs deleted file mode 100644 index 0f6ec64..0000000 --- a/src/imap/flags.rs +++ /dev/null @@ -1,30 +0,0 @@ -use imap_codec::imap_types::core::Atom; -use imap_codec::imap_types::flag::{Flag, FlagFetch}; - -pub fn from_str(f: &str) -> Option<FlagFetch<'static>> { - match f.chars().next() { - Some('\\') => match f { - "\\Seen" => Some(FlagFetch::Flag(Flag::Seen)), - "\\Answered" => Some(FlagFetch::Flag(Flag::Answered)), - "\\Flagged" => Some(FlagFetch::Flag(Flag::Flagged)), - "\\Deleted" => Some(FlagFetch::Flag(Flag::Deleted)), - "\\Draft" => Some(FlagFetch::Flag(Flag::Draft)), - "\\Recent" => Some(FlagFetch::Recent), - _ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) { - Err(_) => { - tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); - None - } - Ok(a) => Some(FlagFetch::Flag(Flag::system(a))), - }, - }, - Some(_) => match Atom::try_from(f.to_string()) { - Err(_) => { - tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); - None - } - Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))), - }, - None => None, - } -} diff --git a/src/imap/flow.rs b/src/imap/flow.rs deleted file mode 100644 index 86eb12e..0000000 --- a/src/imap/flow.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::error::Error as StdError; -use std::fmt; -use std::sync::Arc; - -use imap_codec::imap_types::core::Tag; -use tokio::sync::Notify; - -use crate::imap::mailbox_view::MailboxView; -use crate::user::User; - -#[derive(Debug)] -pub enum Error { - ForbiddenTransition, -} -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Forbidden Transition") - } -} -impl StdError for Error {} - -pub enum State { - NotAuthenticated, - Authenticated(Arc<User>), - Selected(Arc<User>, MailboxView, MailboxPerm), - Idle( - Arc<User>, - MailboxView, - MailboxPerm, - Tag<'static>, - Arc<Notify>, - ), - Logout, -} -impl State { - pub fn notify(&self) -> Option<Arc<Notify>> { - match self { - Self::Idle(_, _, _, _, anotif) => Some(anotif.clone()), - _ => None, - } - } -} -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use State::*; - match self { - NotAuthenticated => write!(f, "NotAuthenticated"), - Authenticated(..) => write!(f, "Authenticated"), - Selected(..) => write!(f, "Selected"), - Idle(..) => write!(f, "Idle"), - Logout => write!(f, "Logout"), - } - } -} - -#[derive(Clone)] -pub enum MailboxPerm { - ReadOnly, - ReadWrite, -} - -pub enum Transition { - None, - Authenticate(Arc<User>), - Select(MailboxView, MailboxPerm), - Idle(Tag<'static>, Notify), - UnIdle, - Unselect, - Logout, -} -impl fmt::Display for Transition { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Transition::*; - match self { - None => write!(f, "None"), - Authenticate(..) => write!(f, "Authenticated"), - Select(..) => write!(f, "Selected"), - Idle(..) => write!(f, "Idle"), - UnIdle => write!(f, "UnIdle"), - Unselect => write!(f, "Unselect"), - Logout => write!(f, "Logout"), - } - } -} - -// See RFC3501 section 3. -// https://datatracker.ietf.org/doc/html/rfc3501#page-13 -impl State { - pub fn apply(&mut self, tr: Transition) -> Result<(), Error> { - tracing::debug!(state=%self, transition=%tr, "try change state"); - - let new_state = match (std::mem::replace(self, State::Logout), tr) { - (s, Transition::None) => s, - (State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u), - (State::Authenticated(u) | State::Selected(u, _, _), Transition::Select(m, p)) => { - State::Selected(u, m, p) - } - (State::Selected(u, _, _), Transition::Unselect) => State::Authenticated(u.clone()), - (State::Selected(u, m, p), Transition::Idle(t, s)) => { - State::Idle(u, m, p, t, Arc::new(s)) - } - (State::Idle(u, m, p, _, _), Transition::UnIdle) => State::Selected(u, m, p), - (_, Transition::Logout) => State::Logout, - (s, t) => { - tracing::error!(state=%s, transition=%t, "forbidden transition"); - return Err(Error::ForbiddenTransition); - } - }; - *self = new_state; - tracing::debug!(state=%self, "transition succeeded"); - - Ok(()) - } -} diff --git a/src/imap/imf_view.rs b/src/imap/imf_view.rs deleted file mode 100644 index a4ca2e8..0000000 --- a/src/imap/imf_view.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anyhow::{anyhow, Result}; -use chrono::naive::NaiveDate; - -use imap_codec::imap_types::core::{IString, NString}; -use imap_codec::imap_types::envelope::{Address, Envelope}; - -use eml_codec::imf; - -pub struct ImfView<'a>(pub &'a imf::Imf<'a>); - -impl<'a> ImfView<'a> { - pub fn naive_date(&self) -> Result<NaiveDate> { - Ok(self.0.date.ok_or(anyhow!("date is not set"))?.date_naive()) - } - - /// Envelope rules are defined in RFC 3501, section 7.4.2 - /// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2 - /// - /// Some important notes: - /// - /// If the Sender or Reply-To lines are absent in the [RFC-2822] - /// header, or are present but empty, the server sets the - /// corresponding member of the envelope to be the same value as - /// the from member (the client is not expected to know to do - /// this). Note: [RFC-2822] requires that all messages have a valid - /// From header. Therefore, the from, sender, and reply-to - /// members in the envelope can not be NIL. - /// - /// If the Date, Subject, In-Reply-To, and Message-ID header lines - /// are absent in the [RFC-2822] header, the corresponding member - /// of the envelope is NIL; if these header lines are present but - /// empty the corresponding member of the envelope is the empty - /// string. - - //@FIXME return an error if the envelope is invalid instead of panicking - //@FIXME some fields must be defaulted if there are not set. - pub fn message_envelope(&self) -> Envelope<'static> { - let msg = self.0; - let from = msg.from.iter().map(convert_mbx).collect::<Vec<_>>(); - - Envelope { - date: NString( - msg.date - .as_ref() - .map(|d| IString::try_from(d.to_rfc3339()).unwrap()), - ), - subject: NString( - msg.subject - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - sender: msg - .sender - .as_ref() - .map(|v| vec![convert_mbx(v)]) - .unwrap_or(from.clone()), - reply_to: if msg.reply_to.is_empty() { - from.clone() - } else { - convert_addresses(&msg.reply_to) - }, - from, - to: convert_addresses(&msg.to), - cc: convert_addresses(&msg.cc), - bcc: convert_addresses(&msg.bcc), - in_reply_to: NString( - msg.in_reply_to - .iter() - .next() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - message_id: NString( - msg.msg_id - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - } - } -} - -pub fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address<'static>> { - let mut acc = vec![]; - for item in addrlist { - match item { - imf::address::AddressRef::Single(a) => acc.push(convert_mbx(a)), - imf::address::AddressRef::Many(l) => acc.extend(l.participants.iter().map(convert_mbx)), - } - } - return acc; -} - -pub fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address<'static> { - Address { - name: NString( - addr.name - .as_ref() - .map(|x| IString::try_from(x.to_string()).unwrap()), - ), - // SMTP at-domain-list (source route) seems obsolete since at least 1991 - // https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html - adl: NString(None), - mailbox: NString(Some( - IString::try_from(addr.addrspec.local_part.to_string()).unwrap(), - )), - host: NString(Some( - IString::try_from(addr.addrspec.domain.to_string()).unwrap(), - )), - } -} diff --git a/src/imap/index.rs b/src/imap/index.rs deleted file mode 100644 index 9b794b8..0000000 --- a/src/imap/index.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::num::{NonZeroU32, NonZeroU64}; - -use anyhow::{anyhow, Result}; -use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; - -use crate::mail::uidindex::{ImapUid, ModSeq, UidIndex}; -use crate::mail::unique_ident::UniqueIdent; - -pub struct Index<'a> { - pub imap_index: Vec<MailIndex<'a>>, - pub internal: &'a UidIndex, -} -impl<'a> Index<'a> { - pub fn new(internal: &'a UidIndex) -> Result<Self> { - let imap_index = internal - .idx_by_uid - .iter() - .enumerate() - .map(|(i_enum, (&uid, &uuid))| { - let (_, modseq, flags) = internal - .table - .get(&uuid) - .ok_or(anyhow!("mail is missing from index"))?; - let i_int: u32 = (i_enum + 1).try_into()?; - let i: NonZeroU32 = i_int.try_into()?; - - Ok(MailIndex { - i, - uid, - uuid, - modseq: *modseq, - flags, - }) - }) - .collect::<Result<Vec<_>>>()?; - - Ok(Self { - imap_index, - internal, - }) - } - - pub fn last(&'a self) -> Option<&'a MailIndex<'a>> { - self.imap_index.last() - } - - /// Fetch mail descriptors based on a sequence of UID - /// - /// Complexity analysis: - /// - Sort is O(n * log n) where n is the number of uid generated by the sequence - /// - Finding the starting point in the index O(log m) where m is the size of the mailbox - /// While n =< m, it's not clear if the difference is big or not. - /// - /// For now, the algorithm tries to be fast for small values of n, - /// as it is what is expected by clients. - /// - /// So we assume for our implementation that : n << m. - /// It's not true for full mailbox searches for example... - pub fn fetch_on_uid(&'a self, sequence_set: &SequenceSet) -> Vec<&'a MailIndex<'a>> { - if self.imap_index.is_empty() { - return vec![]; - } - let largest = self.last().expect("The mailbox is not empty").uid; - let mut unroll_seq = sequence_set.iter(largest).collect::<Vec<_>>(); - unroll_seq.sort(); - - let start_seq = match unroll_seq.iter().next() { - Some(elem) => elem, - None => return vec![], - }; - - // Quickly jump to the right point in the mailbox vector O(log m) instead - // of iterating one by one O(m). Works only because both unroll_seq & imap_index are sorted per uid. - let mut imap_idx = { - let start_idx = self - .imap_index - .partition_point(|mail_idx| &mail_idx.uid < start_seq); - &self.imap_index[start_idx..] - }; - - let mut acc = vec![]; - for wanted_uid in unroll_seq.iter() { - // Slide the window forward as long as its first element is lower than our wanted uid. - let start_idx = match imap_idx.iter().position(|midx| &midx.uid >= wanted_uid) { - Some(v) => v, - None => break, - }; - imap_idx = &imap_idx[start_idx..]; - - // If the beginning of our new window is the uid we want, we collect it - if &imap_idx[0].uid == wanted_uid { - acc.push(&imap_idx[0]); - } - } - - acc - } - - pub fn fetch_on_id(&'a self, sequence_set: &SequenceSet) -> Result<Vec<&'a MailIndex<'a>>> { - if self.imap_index.is_empty() { - return Ok(vec![]); - } - let largest = NonZeroU32::try_from(self.imap_index.len() as u32)?; - let mut acc = sequence_set - .iter(largest) - .map(|wanted_id| { - self.imap_index - .get((wanted_id.get() as usize) - 1) - .ok_or(anyhow!("Mail not found")) - }) - .collect::<Result<Vec<_>>>()?; - - // Sort the result to be consistent with UID - acc.sort_by(|a, b| a.i.cmp(&b.i)); - - Ok(acc) - } - - pub fn fetch( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - by_uid: bool, - ) -> Result<Vec<&'a MailIndex<'a>>> { - match by_uid { - true => Ok(self.fetch_on_uid(sequence_set)), - _ => self.fetch_on_id(sequence_set), - } - } - - pub fn fetch_changed_since( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - maybe_modseq: Option<NonZeroU64>, - by_uid: bool, - ) -> Result<Vec<&'a MailIndex<'a>>> { - let raw = self.fetch(sequence_set, by_uid)?; - let res = match maybe_modseq { - Some(pit) => raw.into_iter().filter(|midx| midx.modseq > pit).collect(), - None => raw, - }; - - Ok(res) - } - - pub fn fetch_unchanged_since( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - maybe_modseq: Option<NonZeroU64>, - by_uid: bool, - ) -> Result<(Vec<&'a MailIndex<'a>>, Vec<&'a MailIndex<'a>>)> { - let raw = self.fetch(sequence_set, by_uid)?; - let res = match maybe_modseq { - Some(pit) => raw.into_iter().partition(|midx| midx.modseq <= pit), - None => (raw, vec![]), - }; - - Ok(res) - } -} - -#[derive(Clone, Debug)] -pub struct MailIndex<'a> { - pub i: NonZeroU32, - pub uid: ImapUid, - pub uuid: UniqueIdent, - pub modseq: ModSeq, - pub flags: &'a Vec<String>, -} - -impl<'a> MailIndex<'a> { - // The following functions are used to implement the SEARCH command - pub fn is_in_sequence_i(&self, seq: &Sequence) -> bool { - match seq { - Sequence::Single(SeqOrUid::Asterisk) => true, - Sequence::Single(SeqOrUid::Value(target)) => target == &self.i, - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) - | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.i, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - if x1 < x2 { - x1 <= &self.i && &self.i <= x2 - } else { - x1 >= &self.i && &self.i >= x2 - } - } - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, - } - } - - pub fn is_in_sequence_uid(&self, seq: &Sequence) -> bool { - match seq { - Sequence::Single(SeqOrUid::Asterisk) => true, - Sequence::Single(SeqOrUid::Value(target)) => target == &self.uid, - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) - | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.uid, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - if x1 < x2 { - x1 <= &self.uid && &self.uid <= x2 - } else { - x1 >= &self.uid && &self.uid >= x2 - } - } - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, - } - } - - pub fn is_flag_set(&self, flag: &str) -> bool { - self.flags - .iter() - .any(|candidate| candidate.as_str() == flag) - } -} diff --git a/src/imap/mail_view.rs b/src/imap/mail_view.rs deleted file mode 100644 index a8db733..0000000 --- a/src/imap/mail_view.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::num::NonZeroU32; - -use anyhow::{anyhow, bail, Result}; -use chrono::{naive::NaiveDate, DateTime as ChronoDateTime, Local, Offset, TimeZone, Utc}; - -use imap_codec::imap_types::core::NString; -use imap_codec::imap_types::datetime::DateTime; -use imap_codec::imap_types::fetch::{ - MessageDataItem, MessageDataItemName, Section as FetchSection, -}; -use imap_codec::imap_types::flag::Flag; -use imap_codec::imap_types::response::Data; - -use eml_codec::{ - imf, - part::{composite::Message, AnyPart}, -}; - -use crate::mail::query::QueryResult; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::flags; -use crate::imap::imf_view::ImfView; -use crate::imap::index::MailIndex; -use crate::imap::mime_view; -use crate::imap::response::Body; - -pub struct MailView<'a> { - pub in_idx: &'a MailIndex<'a>, - pub query_result: &'a QueryResult, - pub content: FetchedMail<'a>, -} - -impl<'a> MailView<'a> { - pub fn new(query_result: &'a QueryResult, in_idx: &'a MailIndex<'a>) -> Result<MailView<'a>> { - Ok(Self { - in_idx, - query_result, - content: match query_result { - QueryResult::FullResult { content, .. } => { - let (_, parsed) = - eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?; - FetchedMail::full_from_message(parsed) - } - QueryResult::PartialResult { metadata, .. } => { - let (_, parsed) = eml_codec::parse_message(&metadata.headers) - .or(Err(anyhow!("unable to parse email headers")))?; - FetchedMail::partial_from_message(parsed) - } - QueryResult::IndexResult { .. } => FetchedMail::IndexOnly, - }, - }) - } - - pub fn imf(&self) -> Option<ImfView> { - self.content.as_imf().map(ImfView) - } - - pub fn selected_mime(&'a self) -> Option<mime_view::SelectedMime<'a>> { - self.content.as_anypart().ok().map(mime_view::SelectedMime) - } - - pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { - let mut seen = SeenFlag::DoNothing; - let res_attrs = ap - .attrs - .iter() - .map(|attr| match attr { - MessageDataItemName::Uid => Ok(self.uid()), - MessageDataItemName::Flags => Ok(self.flags()), - MessageDataItemName::Rfc822Size => self.rfc_822_size(), - MessageDataItemName::Rfc822Header => self.rfc_822_header(), - MessageDataItemName::Rfc822Text => self.rfc_822_text(), - MessageDataItemName::Rfc822 => { - if self.is_not_yet_seen() { - seen = SeenFlag::MustAdd; - } - self.rfc822() - } - MessageDataItemName::Envelope => Ok(self.envelope()), - MessageDataItemName::Body => self.body(), - MessageDataItemName::BodyStructure => self.body_structure(), - MessageDataItemName::BodyExt { - section, - partial, - peek, - } => { - let (body, has_seen) = self.body_ext(section, partial, peek)?; - seen = has_seen; - Ok(body) - } - MessageDataItemName::InternalDate => self.internal_date(), - MessageDataItemName::ModSeq => Ok(self.modseq()), - }) - .collect::<Result<Vec<_>, _>>()?; - - Ok(( - Body::Data(Data::Fetch { - seq: self.in_idx.i, - items: res_attrs.try_into()?, - }), - seen, - )) - } - - pub fn stored_naive_date(&self) -> Result<NaiveDate> { - let mail_meta = self.query_result.metadata().expect("metadata were fetched"); - let mail_ts: i64 = mail_meta.internaldate.try_into()?; - let msg_date: ChronoDateTime<Local> = ChronoDateTime::from_timestamp(mail_ts, 0) - .ok_or(anyhow!("unable to parse timestamp"))? - .with_timezone(&Local); - - Ok(msg_date.date_naive()) - } - - pub fn is_header_contains_pattern(&self, hdr: &[u8], pattern: &[u8]) -> bool { - let mime = match self.selected_mime() { - None => return false, - Some(x) => x, - }; - - let val = match mime.header_value(hdr) { - None => return false, - Some(x) => x, - }; - - val.windows(pattern.len()).any(|win| win == pattern) - } - - // Private function, mainly for filter! - fn uid(&self) -> MessageDataItem<'static> { - MessageDataItem::Uid(self.in_idx.uid.clone()) - } - - fn flags(&self) -> MessageDataItem<'static> { - MessageDataItem::Flags( - self.in_idx - .flags - .iter() - .filter_map(|f| flags::from_str(f)) - .collect(), - ) - } - - fn rfc_822_size(&self) -> Result<MessageDataItem<'static>> { - let sz = self - .query_result - .metadata() - .ok_or(anyhow!("mail metadata are required"))? - .rfc822_size; - Ok(MessageDataItem::Rfc822Size(sz as u32)) - } - - fn rfc_822_header(&self) -> Result<MessageDataItem<'static>> { - let hdrs: NString = self - .query_result - .metadata() - .ok_or(anyhow!("mail metadata are required"))? - .headers - .to_vec() - .try_into()?; - Ok(MessageDataItem::Rfc822Header(hdrs)) - } - - fn rfc_822_text(&self) -> Result<MessageDataItem<'static>> { - let txt: NString = self.content.as_msg()?.raw_body.to_vec().try_into()?; - Ok(MessageDataItem::Rfc822Text(txt)) - } - - fn rfc822(&self) -> Result<MessageDataItem<'static>> { - let full: NString = self.content.as_msg()?.raw_part.to_vec().try_into()?; - Ok(MessageDataItem::Rfc822(full)) - } - - fn envelope(&self) -> MessageDataItem<'static> { - MessageDataItem::Envelope( - self.imf() - .expect("an imf object is derivable from fetchedmail") - .message_envelope(), - ) - } - - fn body(&self) -> Result<MessageDataItem<'static>> { - Ok(MessageDataItem::Body(mime_view::bodystructure( - self.content.as_msg()?.child.as_ref(), - false, - )?)) - } - - fn body_structure(&self) -> Result<MessageDataItem<'static>> { - Ok(MessageDataItem::BodyStructure(mime_view::bodystructure( - self.content.as_msg()?.child.as_ref(), - true, - )?)) - } - - fn is_not_yet_seen(&self) -> bool { - let seen_flag = Flag::Seen.to_string(); - !self.in_idx.flags.iter().any(|x| *x == seen_flag) - } - - /// maps to BODY[<section>]<<partial>> and BODY.PEEK[<section>]<<partial>> - /// peek does not implicitly set the \Seen flag - /// eg. BODY[HEADER.FIELDS (DATE FROM)] - /// eg. BODY[]<0.2048> - fn body_ext( - &self, - section: &Option<FetchSection<'static>>, - partial: &Option<(u32, NonZeroU32)>, - peek: &bool, - ) -> Result<(MessageDataItem<'static>, SeenFlag)> { - // Manage Seen flag - let mut seen = SeenFlag::DoNothing; - if !peek && self.is_not_yet_seen() { - // Add \Seen flag - //self.mailbox.add_flags(uuid, &[seen_flag]).await?; - seen = SeenFlag::MustAdd; - } - - // Process message - let (text, origin) = - match mime_view::body_ext(self.content.as_anypart()?, section, partial)? { - mime_view::BodySection::Full(body) => (body, None), - mime_view::BodySection::Slice { body, origin_octet } => (body, Some(origin_octet)), - }; - - let data: NString = text.to_vec().try_into()?; - - return Ok(( - MessageDataItem::BodyExt { - section: section.as_ref().map(|fs| fs.clone()), - origin, - data, - }, - seen, - )); - } - - fn internal_date(&self) -> Result<MessageDataItem<'static>> { - let dt = Utc - .fix() - .timestamp_opt( - i64::try_from( - self.query_result - .metadata() - .ok_or(anyhow!("mail metadata were not fetched"))? - .internaldate - / 1000, - )?, - 0, - ) - .earliest() - .ok_or(anyhow!("Unable to parse internal date"))?; - Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) - } - - fn modseq(&self) -> MessageDataItem<'static> { - MessageDataItem::ModSeq(self.in_idx.modseq) - } -} - -pub enum SeenFlag { - DoNothing, - MustAdd, -} - -// ------------------- - -pub enum FetchedMail<'a> { - IndexOnly, - Partial(AnyPart<'a>), - Full(AnyPart<'a>), -} -impl<'a> FetchedMail<'a> { - pub fn full_from_message(msg: Message<'a>) -> Self { - Self::Full(AnyPart::Msg(msg)) - } - - pub fn partial_from_message(msg: Message<'a>) -> Self { - Self::Partial(AnyPart::Msg(msg)) - } - - pub fn as_anypart(&self) -> Result<&AnyPart<'a>> { - match self { - FetchedMail::Full(x) => Ok(&x), - FetchedMail::Partial(x) => Ok(&x), - _ => bail!("The full message must be fetched, not only its headers"), - } - } - - pub fn as_msg(&self) -> Result<&Message<'a>> { - match self { - FetchedMail::Full(AnyPart::Msg(x)) => Ok(&x), - FetchedMail::Partial(AnyPart::Msg(x)) => Ok(&x), - _ => bail!("The full message must be fetched, not only its headers AND it must be an AnyPart::Msg."), - } - } - - pub fn as_imf(&self) -> Option<&imf::Imf<'a>> { - match self { - FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf), - FetchedMail::Partial(AnyPart::Msg(x)) => Some(&x.imf), - _ => None, - } - } -} diff --git a/src/imap/mailbox_view.rs b/src/imap/mailbox_view.rs deleted file mode 100644 index 1c53b93..0000000 --- a/src/imap/mailbox_view.rs +++ /dev/null @@ -1,772 +0,0 @@ -use std::collections::HashSet; -use std::num::{NonZeroU32, NonZeroU64}; -use std::sync::Arc; - -use anyhow::{anyhow, Error, Result}; - -use futures::stream::{StreamExt, TryStreamExt}; - -use imap_codec::imap_types::core::{Charset, Vec1}; -use imap_codec::imap_types::fetch::MessageDataItem; -use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; -use imap_codec::imap_types::response::{Code, CodeOther, Data, Status}; -use imap_codec::imap_types::search::SearchKey; -use imap_codec::imap_types::sequence::SequenceSet; - -use crate::mail::mailbox::Mailbox; -use crate::mail::query::QueryScope; -use crate::mail::snapshot::FrozenMailbox; -use crate::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq}; -use crate::mail::unique_ident::UniqueIdent; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::flags; -use crate::imap::index::Index; -use crate::imap::mail_view::{MailView, SeenFlag}; -use crate::imap::response::Body; -use crate::imap::search; - -const DEFAULT_FLAGS: [Flag; 5] = [ - Flag::Seen, - Flag::Answered, - Flag::Flagged, - Flag::Deleted, - Flag::Draft, -]; - -pub struct UpdateParameters { - pub silence: HashSet<UniqueIdent>, - pub with_modseq: bool, - pub with_uid: bool, -} -impl Default for UpdateParameters { - fn default() -> Self { - Self { - silence: HashSet::new(), - with_modseq: false, - with_uid: false, - } - } -} - -/// A MailboxView is responsible for giving the client the information -/// it needs about a mailbox, such as an initial summary of the mailbox's -/// content and continuous updates indicating when the content -/// of the mailbox has been changed. -/// To do this, it keeps a variable `known_state` that corresponds to -/// what the client knows, and produces IMAP messages to be sent to the -/// client that go along updates to `known_state`. -pub struct MailboxView { - pub internal: FrozenMailbox, - pub is_condstore: bool, -} - -impl MailboxView { - /// Creates a new IMAP view into a mailbox. - pub async fn new(mailbox: Arc<Mailbox>, is_cond: bool) -> Self { - Self { - internal: mailbox.frozen().await, - is_condstore: is_cond, - } - } - - /// Create an updated view, useful to make a diff - /// between what the client knows and new stuff - /// Produces a set of IMAP responses describing the change between - /// what the client knows and what is actually in the mailbox. - /// This does NOT trigger a sync, it bases itself on what is currently - /// loaded in RAM by Bayou. - pub async fn update(&mut self, params: UpdateParameters) -> Result<Vec<Body<'static>>> { - let old_snapshot = self.internal.update().await; - let new_snapshot = &self.internal.snapshot; - - let mut data = Vec::<Body>::new(); - - // Calculate diff between two mailbox states - // See example in IMAP RFC in section on NOOP command: - // we want to produce something like this: - // C: a047 NOOP - // S: * 22 EXPUNGE - // S: * 23 EXISTS - // S: * 14 FETCH (UID 1305 FLAGS (\Seen \Deleted)) - // S: a047 OK Noop completed - // In other words: - // - notify client of expunged mails - // - if new mails arrived, notify client of number of existing mails - // - if flags changed for existing mails, tell client - // (for this last step: if uidvalidity changed, do nothing, - // just notify of new uidvalidity and they will resync) - - // - notify client of expunged mails - let mut n_expunge = 0; - for (i, (_uid, uuid)) in old_snapshot.idx_by_uid.iter().enumerate() { - if !new_snapshot.table.contains_key(uuid) { - data.push(Body::Data(Data::Expunge( - NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), - ))); - n_expunge += 1; - } - } - - // - if new mails arrived, notify client of number of existing mails - if new_snapshot.table.len() != old_snapshot.table.len() - n_expunge - || new_snapshot.uidvalidity != old_snapshot.uidvalidity - { - data.push(self.exists_status()?); - } - - if new_snapshot.uidvalidity != old_snapshot.uidvalidity { - // TODO: do we want to push less/more info than this? - data.push(self.uidvalidity_status()?); - data.push(self.uidnext_status()?); - } else { - // - if flags changed for existing mails, tell client - for (i, (_uid, uuid)) in new_snapshot.idx_by_uid.iter().enumerate() { - if params.silence.contains(uuid) { - continue; - } - - let old_mail = old_snapshot.table.get(uuid); - let new_mail = new_snapshot.table.get(uuid); - if old_mail.is_some() && old_mail != new_mail { - if let Some((uid, modseq, flags)) = new_mail { - let mut items = vec![MessageDataItem::Flags( - flags.iter().filter_map(|f| flags::from_str(f)).collect(), - )]; - - if params.with_uid { - items.push(MessageDataItem::Uid(*uid)); - } - - if params.with_modseq { - items.push(MessageDataItem::ModSeq(*modseq)); - } - - data.push(Body::Data(Data::Fetch { - seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), - items: items.try_into()?, - })); - } - } - } - } - Ok(data) - } - - /// Generates the necessary IMAP messages so that the client - /// has a satisfactory summary of the current mailbox's state. - /// These are the messages that are sent in response to a SELECT command. - pub fn summary(&self) -> Result<Vec<Body<'static>>> { - let mut data = Vec::<Body>::new(); - data.push(self.exists_status()?); - data.push(self.recent_status()?); - data.extend(self.flags_status()?.into_iter()); - data.push(self.uidvalidity_status()?); - data.push(self.uidnext_status()?); - if self.is_condstore { - data.push(self.highestmodseq_status()?); - } - /*self.unseen_first_status()? - .map(|unseen_status| data.push(unseen_status));*/ - - Ok(data) - } - - pub async fn store<'a>( - &mut self, - sequence_set: &SequenceSet, - kind: &StoreType, - response: &StoreResponse, - flags: &[Flag<'a>], - unchanged_since: Option<NonZeroU64>, - is_uid_store: &bool, - ) -> Result<(Vec<Body<'static>>, Vec<NonZeroU32>)> { - self.internal.sync().await?; - - let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); - - let idx = self.index()?; - let (editable, in_conflict) = - idx.fetch_unchanged_since(sequence_set, unchanged_since, *is_uid_store)?; - - for mi in editable.iter() { - match kind { - StoreType::Add => { - self.internal.mailbox.add_flags(mi.uuid, &flags[..]).await?; - } - StoreType::Remove => { - self.internal.mailbox.del_flags(mi.uuid, &flags[..]).await?; - } - StoreType::Replace => { - self.internal.mailbox.set_flags(mi.uuid, &flags[..]).await?; - } - } - } - - let silence = match response { - StoreResponse::Answer => HashSet::new(), - StoreResponse::Silent => editable.iter().map(|midx| midx.uuid).collect(), - }; - - let conflict_id_or_uid = match is_uid_store { - true => in_conflict.into_iter().map(|midx| midx.uid).collect(), - _ => in_conflict.into_iter().map(|midx| midx.i).collect(), - }; - - let summary = self - .update(UpdateParameters { - with_uid: *is_uid_store, - with_modseq: unchanged_since.is_some(), - silence, - }) - .await?; - - Ok((summary, conflict_id_or_uid)) - } - - pub async fn idle_sync(&mut self) -> Result<Vec<Body<'static>>> { - self.internal - .mailbox - .notify() - .await - .upgrade() - .ok_or(anyhow!("test"))? - .notified() - .await; - self.internal.mailbox.opportunistic_sync().await?; - self.update(UpdateParameters::default()).await - } - - pub async fn expunge( - &mut self, - maybe_seq_set: &Option<SequenceSet>, - ) -> Result<Vec<Body<'static>>> { - // Get a recent view to apply our change - self.internal.sync().await?; - let state = self.internal.peek().await; - let idx = Index::new(&state)?; - - // Build a default sequence set for the default case - use imap_codec::imap_types::sequence::{SeqOrUid, Sequence}; - let seq = match maybe_seq_set { - Some(s) => s.clone(), - None => SequenceSet( - vec![Sequence::Range( - SeqOrUid::Value(NonZeroU32::MIN), - SeqOrUid::Asterisk, - )] - .try_into() - .unwrap(), - ), - }; - - let deleted_flag = Flag::Deleted.to_string(); - let msgs = idx - .fetch_on_uid(&seq) - .into_iter() - .filter(|midx| midx.flags.iter().any(|x| *x == deleted_flag)) - .map(|midx| midx.uuid); - - for msg in msgs { - self.internal.mailbox.delete(msg).await?; - } - - self.update(UpdateParameters::default()).await - } - - pub async fn copy( - &self, - sequence_set: &SequenceSet, - to: Arc<Mailbox>, - is_uid_copy: &bool, - ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>)> { - let idx = self.index()?; - let mails = idx.fetch(sequence_set, *is_uid_copy)?; - - let mut new_uuids = vec![]; - for mi in mails.iter() { - new_uuids.push(to.copy_from(&self.internal.mailbox, mi.uuid).await?); - } - - let mut ret = vec![]; - let to_state = to.current_uid_index().await; - for (mi, new_uuid) in mails.iter().zip(new_uuids.iter()) { - let dest_uid = to_state - .table - .get(new_uuid) - .ok_or(anyhow!("copied mail not in destination mailbox"))? - .0; - ret.push((mi.uid, dest_uid)); - } - - Ok((to_state.uidvalidity, ret)) - } - - pub async fn r#move( - &mut self, - sequence_set: &SequenceSet, - to: Arc<Mailbox>, - is_uid_copy: &bool, - ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec<Body<'static>>)> { - let idx = self.index()?; - let mails = idx.fetch(sequence_set, *is_uid_copy)?; - - for mi in mails.iter() { - to.move_from(&self.internal.mailbox, mi.uuid).await?; - } - - let mut ret = vec![]; - let to_state = to.current_uid_index().await; - for mi in mails.iter() { - let dest_uid = to_state - .table - .get(&mi.uuid) - .ok_or(anyhow!("moved mail not in destination mailbox"))? - .0; - ret.push((mi.uid, dest_uid)); - } - - let update = self - .update(UpdateParameters { - with_uid: *is_uid_copy, - ..UpdateParameters::default() - }) - .await?; - - Ok((to_state.uidvalidity, ret, update)) - } - - /// Looks up state changes in the mailbox and produces a set of IMAP - /// responses describing the new state. - pub async fn fetch<'b>( - &self, - sequence_set: &SequenceSet, - ap: &AttributesProxy, - changed_since: Option<NonZeroU64>, - is_uid_fetch: &bool, - ) -> Result<Vec<Body<'static>>> { - // [1/6] Pre-compute data - // a. what are the uuids of the emails we want? - // b. do we need to fetch the full body? - //let ap = AttributesProxy::new(attributes, *is_uid_fetch); - let query_scope = match ap.need_body() { - true => QueryScope::Full, - _ => QueryScope::Partial, - }; - tracing::debug!("Query scope {:?}", query_scope); - let idx = self.index()?; - let mail_idx_list = idx.fetch_changed_since(sequence_set, changed_since, *is_uid_fetch)?; - - // [2/6] Fetch the emails - let uuids = mail_idx_list - .iter() - .map(|midx| midx.uuid) - .collect::<Vec<_>>(); - - let query = self.internal.query(&uuids, query_scope); - //let query_result = self.internal.query(&uuids, query_scope).fetch().await?; - - let query_stream = query - .fetch() - .zip(futures::stream::iter(mail_idx_list)) - // [3/6] Derive an IMAP-specific view from the results, apply the filters - .map(|(maybe_qr, midx)| match maybe_qr { - Ok(qr) => Ok((MailView::new(&qr, midx)?.filter(&ap)?, midx)), - Err(e) => Err(e), - }) - // [4/6] Apply the IMAP transformation - .then(|maybe_ret| async move { - let ((body, seen), midx) = maybe_ret?; - - // [5/6] Register the \Seen flags - if matches!(seen, SeenFlag::MustAdd) { - let seen_flag = Flag::Seen.to_string(); - self.internal - .mailbox - .add_flags(midx.uuid, &[seen_flag]) - .await?; - } - - Ok::<_, anyhow::Error>(body) - }); - - // [6/6] Build the final result that will be sent to the client. - query_stream.try_collect().await - } - - /// A naive search implementation... - pub async fn search<'a>( - &self, - _charset: &Option<Charset<'a>>, - search_key: &SearchKey<'a>, - uid: bool, - ) -> Result<(Vec<Body<'static>>, bool)> { - // 1. Compute the subset of sequence identifiers we need to fetch - // based on the search query - let crit = search::Criteria(search_key); - let (seq_set, seq_type) = crit.to_sequence_set(); - - // 2. Get the selection - let idx = self.index()?; - let selection = idx.fetch(&seq_set, seq_type.is_uid())?; - - // 3. Filter the selection based on the ID / UID / Flags - let (kept_idx, to_fetch) = crit.filter_on_idx(&selection); - - // 4.a Fetch additional info about the emails - let query_scope = crit.query_scope(); - let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::<Vec<_>>(); - let query = self.internal.query(&uuids, query_scope); - - // 4.b We don't want to keep all data in memory, so we do the computing in a stream - let query_stream = query - .fetch() - .zip(futures::stream::iter(&to_fetch)) - // 5.a Build a mailview with the body, might fail with an error - // 5.b If needed, filter the selection based on the body, but keep the errors - // 6. Drop the query+mailbox, keep only the mail index - // Here we release a lot of memory, this is the most important part ^^ - .filter_map(|(maybe_qr, midx)| { - let r = match maybe_qr { - Ok(qr) => match MailView::new(&qr, midx).map(|mv| crit.is_keep_on_query(&mv)) { - Ok(true) => Some(Ok(*midx)), - Ok(_) => None, - Err(e) => Some(Err(e)), - }, - Err(e) => Some(Err(e)), - }; - futures::future::ready(r) - }); - - // 7. Chain both streams (part resolved from index, part resolved from metadata+body) - let main_stream = futures::stream::iter(kept_idx) - .map(Ok) - .chain(query_stream) - .map_ok(|idx| match uid { - true => (idx.uid, idx.modseq), - _ => (idx.i, idx.modseq), - }); - - // 8. Do the actual computation - let internal_result: Vec<_> = main_stream.try_collect().await?; - let (selection, modseqs): (Vec<_>, Vec<_>) = internal_result.into_iter().unzip(); - - // 9. Aggregate the maximum modseq value - let maybe_modseq = match crit.is_modseq() { - true => modseqs.into_iter().max(), - _ => None, - }; - - // 10. Return the final result - Ok(( - vec![Body::Data(Data::Search(selection, maybe_modseq))], - maybe_modseq.is_some(), - )) - } - - // ---- - /// @FIXME index should be stored for longer than a single request - /// Instead they should be tied to the FrozenMailbox refresh - /// It's not trivial to refactor the code to do that, so we are doing - /// some useless computation for now... - fn index<'a>(&'a self) -> Result<Index<'a>> { - Index::new(&self.internal.snapshot) - } - - /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` - fn uidvalidity_status(&self) -> Result<Body<'static>> { - let uid_validity = Status::ok( - None, - Some(Code::UidValidity(self.uidvalidity())), - "UIDs valid", - ) - .map_err(Error::msg)?; - Ok(Body::Status(uid_validity)) - } - - pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { - self.internal.snapshot.uidvalidity - } - - /// Produce an OK [UIDNEXT _] message corresponding to `known_state` - fn uidnext_status(&self) -> Result<Body<'static>> { - let next_uid = Status::ok( - None, - Some(Code::UidNext(self.uidnext())), - "Predict next UID", - ) - .map_err(Error::msg)?; - Ok(Body::Status(next_uid)) - } - - pub(crate) fn uidnext(&self) -> ImapUid { - self.internal.snapshot.uidnext - } - - pub(crate) fn highestmodseq_status(&self) -> Result<Body<'static>> { - Ok(Body::Status(Status::ok( - None, - Some(Code::Other(CodeOther::unvalidated( - format!("HIGHESTMODSEQ {}", self.highestmodseq()).into_bytes(), - ))), - "Highest", - )?)) - } - - pub(crate) fn highestmodseq(&self) -> ModSeq { - self.internal.snapshot.highestmodseq - } - - /// Produce an EXISTS message corresponding to the number of mails - /// in `known_state` - fn exists_status(&self) -> Result<Body<'static>> { - Ok(Body::Data(Data::Exists(self.exists()?))) - } - - pub(crate) fn exists(&self) -> Result<u32> { - Ok(u32::try_from(self.internal.snapshot.idx_by_uid.len())?) - } - - /// Produce a RECENT message corresponding to the number of - /// recent mails in `known_state` - fn recent_status(&self) -> Result<Body<'static>> { - Ok(Body::Data(Data::Recent(self.recent()?))) - } - - #[allow(dead_code)] - fn unseen_first_status(&self) -> Result<Option<Body<'static>>> { - Ok(self - .unseen_first()? - .map(|unseen_id| { - Status::ok(None, Some(Code::Unseen(unseen_id)), "First unseen.").map(Body::Status) - }) - .transpose()?) - } - - #[allow(dead_code)] - fn unseen_first(&self) -> Result<Option<NonZeroU32>> { - Ok(self - .internal - .snapshot - .table - .values() - .enumerate() - .find(|(_i, (_imap_uid, _modseq, flags))| !flags.contains(&"\\Seen".to_string())) - .map(|(i, _)| NonZeroU32::try_from(i as u32 + 1)) - .transpose()?) - } - - pub(crate) fn recent(&self) -> Result<u32> { - let recent = self - .internal - .snapshot - .idx_by_flag - .get(&"\\Recent".to_string()) - .map(|os| os.len()) - .unwrap_or(0); - Ok(u32::try_from(recent)?) - } - - /// Produce a FLAGS and a PERMANENTFLAGS message that indicates - /// the flags that are in `known_state` + default flags - fn flags_status(&self) -> Result<Vec<Body<'static>>> { - let mut body = vec![]; - - // 1. Collecting all the possible flags in the mailbox - // 1.a Fetch them from our index - let mut known_flags: Vec<Flag> = self - .internal - .snapshot - .idx_by_flag - .flags() - .filter_map(|f| match flags::from_str(f) { - Some(FlagFetch::Flag(fl)) => Some(fl), - _ => None, - }) - .collect(); - // 1.b Merge it with our default flags list - for f in DEFAULT_FLAGS.iter() { - if !known_flags.contains(f) { - known_flags.push(f.clone()); - } - } - // 1.c Create the IMAP message - body.push(Body::Data(Data::Flags(known_flags.clone()))); - - // 2. Returning flags that are persisted - // 2.a Always advertise our default flags - let mut permanent = DEFAULT_FLAGS - .iter() - .map(|f| FlagPerm::Flag(f.clone())) - .collect::<Vec<_>>(); - // 2.b Say that we support any keyword flag - permanent.push(FlagPerm::Asterisk); - // 2.c Create the IMAP message - let permanent_flags = Status::ok( - None, - Some(Code::PermanentFlags(permanent)), - "Flags permitted", - ) - .map_err(Error::msg)?; - body.push(Body::Status(permanent_flags)); - - // Done! - Ok(body) - } - - pub(crate) fn unseen_count(&self) -> usize { - let total = self.internal.snapshot.table.len(); - let seen = self - .internal - .snapshot - .idx_by_flag - .get(&Flag::Seen.to_string()) - .map(|x| x.len()) - .unwrap_or(0); - total - seen - } -} - -#[cfg(test)] -mod tests { - use super::*; - use imap_codec::encode::Encoder; - use imap_codec::imap_types::core::Vec1; - use imap_codec::imap_types::fetch::Section; - use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName}; - use imap_codec::imap_types::response::Response; - use imap_codec::ResponseCodec; - use std::fs; - - use crate::cryptoblob; - use crate::imap::index::MailIndex; - use crate::imap::mail_view::MailView; - use crate::imap::mime_view; - use crate::mail::mailbox::MailMeta; - use crate::mail::query::QueryResult; - use crate::mail::unique_ident; - - #[test] - fn mailview_body_ext() -> Result<()> { - let ap = AttributesProxy::new( - &MacroOrMessageDataItemNames::MessageDataItemNames(vec![ - MessageDataItemName::BodyExt { - section: Some(Section::Header(None)), - partial: None, - peek: false, - }, - ]), - &[], - false, - ); - - let key = cryptoblob::gen_key(); - let meta = MailMeta { - internaldate: 0u64, - headers: vec![], - message_key: key, - rfc822_size: 8usize, - }; - - let index_entry = (NonZeroU32::MIN, NonZeroU64::MIN, vec![]); - let mail_in_idx = MailIndex { - i: NonZeroU32::MIN, - uid: index_entry.0, - modseq: index_entry.1, - uuid: unique_ident::gen_ident(), - flags: &index_entry.2, - }; - let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; - let qr = QueryResult::FullResult { - uuid: mail_in_idx.uuid.clone(), - metadata: meta, - content: rfc822.to_vec(), - }; - - let mv = MailView::new(&qr, &mail_in_idx)?; - let (res_body, _seen) = mv.filter(&ap)?; - - let fattr = match res_body { - Body::Data(Data::Fetch { - seq: _seq, - items: attr, - }) => Ok(attr), - _ => Err(anyhow!("Not a fetch body")), - }?; - - assert_eq!(fattr.as_ref().len(), 1); - - let (sec, _orig, _data) = match &fattr.as_ref()[0] { - MessageDataItem::BodyExt { - section, - origin, - data, - } => Ok((section, origin, data)), - _ => Err(anyhow!("not a body ext message attribute")), - }?; - - assert_eq!(sec.as_ref().unwrap(), &Section::Header(None)); - - Ok(()) - } - - /// Future automated test. We use lossy utf8 conversion + lowercase everything, - /// so this test might allow invalid results. But at least it allows us to quickly test a - /// large variety of emails. - /// Keep in mind that special cases must still be tested manually! - #[test] - fn fetch_body() -> Result<()> { - let prefixes = [ - /* *** MY OWN DATASET *** */ - "tests/emails/dxflrs/0001_simple", - "tests/emails/dxflrs/0002_mime", - "tests/emails/dxflrs/0003_mime-in-mime", - "tests/emails/dxflrs/0004_msg-in-msg", - // eml_codec do not support continuation for the moment - //"tests/emails/dxflrs/0005_mail-parser-readme", - "tests/emails/dxflrs/0006_single-mime", - "tests/emails/dxflrs/0007_raw_msg_in_rfc822", - /* *** (STRANGE) RFC *** */ - //"tests/emails/rfc/000", // must return text/enriched, we return text/plain - //"tests/emails/rfc/001", // does not recognize the multipart/external-body, breaks the - // whole parsing - //"tests/emails/rfc/002", // wrong date in email - - //"tests/emails/rfc/003", // dovecot fixes \r\r: the bytes number is wrong + text/enriched - - /* *** THIRD PARTY *** */ - //"tests/emails/thirdparty/000", // dovecot fixes \r\r: the bytes number is wrong - //"tests/emails/thirdparty/001", // same - "tests/emails/thirdparty/002", // same - - /* *** LEGACY *** */ - //"tests/emails/legacy/000", // same issue with \r\r - ]; - - for pref in prefixes.iter() { - println!("{}", pref); - let txt = fs::read(format!("{}.eml", pref))?; - let oracle = fs::read(format!("{}.dovecot.body", pref))?; - let message = eml_codec::parse_message(&txt).unwrap().1; - - let test_repr = Response::Data(Data::Fetch { - seq: NonZeroU32::new(1).unwrap(), - items: Vec1::from(MessageDataItem::Body(mime_view::bodystructure( - &message.child, - false, - )?)), - }); - let test_bytes = ResponseCodec::new().encode(&test_repr).dump(); - let test_str = String::from_utf8_lossy(&test_bytes).to_lowercase(); - - let oracle_str = - format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).to_lowercase(); - - println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str); - //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); - assert_eq!(test_str, oracle_str); - } - - Ok(()) - } -} diff --git a/src/imap/mime_view.rs b/src/imap/mime_view.rs deleted file mode 100644 index 8bbbd2d..0000000 --- a/src/imap/mime_view.rs +++ /dev/null @@ -1,580 +0,0 @@ -use std::borrow::Cow; -use std::collections::HashSet; -use std::num::NonZeroU32; - -use anyhow::{anyhow, bail, Result}; - -use imap_codec::imap_types::body::{ - BasicFields, Body as FetchBody, BodyStructure, MultiPartExtensionData, SinglePartExtensionData, - SpecificFields, -}; -use imap_codec::imap_types::core::{AString, IString, NString, Vec1}; -use imap_codec::imap_types::fetch::{Part as FetchPart, Section as FetchSection}; - -use eml_codec::{ - header, mime, mime::r#type::Deductible, part::composite, part::discrete, part::AnyPart, -}; - -use crate::imap::imf_view::ImfView; - -pub enum BodySection<'a> { - Full(Cow<'a, [u8]>), - Slice { - body: Cow<'a, [u8]>, - origin_octet: u32, - }, -} - -/// Logic for BODY[<section>]<<partial>> -/// Works in 3 times: -/// 1. Find the section (RootMime::subset) -/// 2. Apply the extraction logic (SelectedMime::extract), like TEXT, HEADERS, etc. -/// 3. Keep only the given subset provided by partial -/// -/// Example of message sections: -/// -/// ``` -/// HEADER ([RFC-2822] header of the message) -/// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 1 TEXT/PLAIN -/// 2 APPLICATION/OCTET-STREAM -/// 3 MESSAGE/RFC822 -/// 3.HEADER ([RFC-2822] header of the message) -/// 3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 3.1 TEXT/PLAIN -/// 3.2 APPLICATION/OCTET-STREAM -/// 4 MULTIPART/MIXED -/// 4.1 IMAGE/GIF -/// 4.1.MIME ([MIME-IMB] header for the IMAGE/GIF) -/// 4.2 MESSAGE/RFC822 -/// 4.2.HEADER ([RFC-2822] header of the message) -/// 4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 4.2.1 TEXT/PLAIN -/// 4.2.2 MULTIPART/ALTERNATIVE -/// 4.2.2.1 TEXT/PLAIN -/// 4.2.2.2 TEXT/RICHTEXT -/// ``` -pub fn body_ext<'a>( - part: &'a AnyPart<'a>, - section: &'a Option<FetchSection<'a>>, - partial: &'a Option<(u32, NonZeroU32)>, -) -> Result<BodySection<'a>> { - let root_mime = NodeMime(part); - let (extractor, path) = SubsettedSection::from(section); - let selected_mime = root_mime.subset(path)?; - let extracted_full = selected_mime.extract(&extractor)?; - Ok(extracted_full.to_body_section(partial)) -} - -/// Logic for BODY and BODYSTRUCTURE -/// -/// ```raw -/// b fetch 29878:29879 (BODY) -/// * 29878 FETCH (BODY (("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 3264 82)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 31834 643) "alternative")) -/// * 29879 FETCH (BODY ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 4107 131)) -/// ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^^^ ^^^ -/// | | | | | | number of lines -/// | | | | | size -/// | | | | content transfer encoding -/// | | | description -/// | | id -/// | parameter list -/// b OK Fetch completed (0.001 + 0.000 secs). -/// ``` -pub fn bodystructure(part: &AnyPart, is_ext: bool) -> Result<BodyStructure<'static>> { - NodeMime(part).structure(is_ext) -} - -/// NodeMime -/// -/// Used for recursive logic on MIME. -/// See SelectedMime for inspection. -struct NodeMime<'a>(&'a AnyPart<'a>); -impl<'a> NodeMime<'a> { - /// A MIME object is a tree of elements. - /// The path indicates which element must be picked. - /// This function returns the picked element as the new view - fn subset(self, path: Option<&'a FetchPart>) -> Result<SelectedMime<'a>> { - match path { - None => Ok(SelectedMime(self.0)), - Some(v) => self.rec_subset(v.0.as_ref()), - } - } - - fn rec_subset(self, path: &'a [NonZeroU32]) -> Result<SelectedMime> { - if path.is_empty() { - Ok(SelectedMime(self.0)) - } else { - match self.0 { - AnyPart::Mult(x) => { - let next = Self(x.children - .get(path[0].get() as usize - 1) - .ok_or(anyhow!("Unable to resolve subpath {:?}, current multipart has only {} elements", path, x.children.len()))?); - next.rec_subset(&path[1..]) - }, - AnyPart::Msg(x) => { - let next = Self(x.child.as_ref()); - next.rec_subset(path) - }, - _ => bail!("You tried to access a subpart on an atomic part (text or binary). Unresolved subpath {:?}", path), - } - } - } - - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - match self.0 { - AnyPart::Txt(x) => NodeTxt(self, x).structure(is_ext), - AnyPart::Bin(x) => NodeBin(self, x).structure(is_ext), - AnyPart::Mult(x) => NodeMult(self, x).structure(is_ext), - AnyPart::Msg(x) => NodeMsg(self, x).structure(is_ext), - } - } -} - -//---------------------------------------------------------- - -/// A FetchSection must be handled in 2 times: -/// - First we must extract the MIME part -/// - Then we must process it as desired -/// The given struct mixes both work, so -/// we separate this work here. -enum SubsettedSection<'a> { - Part, - Header, - HeaderFields(&'a Vec1<AString<'a>>), - HeaderFieldsNot(&'a Vec1<AString<'a>>), - Text, - Mime, -} -impl<'a> SubsettedSection<'a> { - fn from(section: &'a Option<FetchSection>) -> (Self, Option<&'a FetchPart>) { - match section { - Some(FetchSection::Text(maybe_part)) => (Self::Text, maybe_part.as_ref()), - Some(FetchSection::Header(maybe_part)) => (Self::Header, maybe_part.as_ref()), - Some(FetchSection::HeaderFields(maybe_part, fields)) => { - (Self::HeaderFields(fields), maybe_part.as_ref()) - } - Some(FetchSection::HeaderFieldsNot(maybe_part, fields)) => { - (Self::HeaderFieldsNot(fields), maybe_part.as_ref()) - } - Some(FetchSection::Mime(part)) => (Self::Mime, Some(part)), - Some(FetchSection::Part(part)) => (Self::Part, Some(part)), - None => (Self::Part, None), - } - } -} - -/// Used for current MIME inspection -/// -/// See NodeMime for recursive logic -pub struct SelectedMime<'a>(pub &'a AnyPart<'a>); -impl<'a> SelectedMime<'a> { - pub fn header_value(&'a self, to_match_ext: &[u8]) -> Option<&'a [u8]> { - let to_match = to_match_ext.to_ascii_lowercase(); - - self.eml_mime() - .kv - .iter() - .filter_map(|field| match field { - header::Field::Good(header::Kv2(k, v)) => Some((k, v)), - _ => None, - }) - .find(|(k, _)| k.to_ascii_lowercase() == to_match) - .map(|(_, v)| v) - .copied() - } - - /// The subsetted fetch section basically tells us the - /// extraction logic to apply on our selected MIME. - /// This function acts as a router for these logic. - fn extract(&self, extractor: &SubsettedSection<'a>) -> Result<ExtractedFull<'a>> { - match extractor { - SubsettedSection::Text => self.text(), - SubsettedSection::Header => self.header(), - SubsettedSection::HeaderFields(fields) => self.header_fields(fields, false), - SubsettedSection::HeaderFieldsNot(fields) => self.header_fields(fields, true), - SubsettedSection::Part => self.part(), - SubsettedSection::Mime => self.mime(), - } - } - - fn mime(&self) -> Result<ExtractedFull<'a>> { - let bytes = match &self.0 { - AnyPart::Txt(p) => p.mime.fields.raw, - AnyPart::Bin(p) => p.mime.fields.raw, - AnyPart::Msg(p) => p.child.mime().raw, - AnyPart::Mult(p) => p.mime.fields.raw, - }; - Ok(ExtractedFull(bytes.into())) - } - - fn part(&self) -> Result<ExtractedFull<'a>> { - let bytes = match &self.0 { - AnyPart::Txt(p) => p.body, - AnyPart::Bin(p) => p.body, - AnyPart::Msg(p) => p.raw_part, - AnyPart::Mult(_) => bail!("Multipart part has no body"), - }; - Ok(ExtractedFull(bytes.to_vec().into())) - } - - fn eml_mime(&self) -> &eml_codec::mime::NaiveMIME<'_> { - match &self.0 { - AnyPart::Msg(msg) => msg.child.mime(), - other => other.mime(), - } - } - - /// The [...] HEADER.FIELDS, and HEADER.FIELDS.NOT part - /// specifiers refer to the [RFC-2822] header of the message or of - /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. - /// HEADER.FIELDS and HEADER.FIELDS.NOT are followed by a list of - /// field-name (as defined in [RFC-2822]) names, and return a - /// subset of the header. The subset returned by HEADER.FIELDS - /// contains only those header fields with a field-name that - /// matches one of the names in the list; similarly, the subset - /// returned by HEADER.FIELDS.NOT contains only the header fields - /// with a non-matching field-name. The field-matching is - /// case-insensitive but otherwise exact. - fn header_fields( - &self, - fields: &'a Vec1<AString<'a>>, - invert: bool, - ) -> Result<ExtractedFull<'a>> { - // Build a lowercase ascii hashset with the fields to fetch - let index = fields - .as_ref() - .iter() - .map(|x| { - match x { - AString::Atom(a) => a.inner().as_bytes(), - AString::String(IString::Literal(l)) => l.as_ref(), - AString::String(IString::Quoted(q)) => q.inner().as_bytes(), - } - .to_ascii_lowercase() - }) - .collect::<HashSet<_>>(); - - // Extract MIME headers - let mime = self.eml_mime(); - - // Filter our MIME headers based on the field index - // 1. Keep only the correctly formatted headers - // 2. Keep only based on the index presence or absence - // 3. Reduce as a byte vector - let buffer = mime - .kv - .iter() - .filter_map(|field| match field { - header::Field::Good(header::Kv2(k, v)) => Some((k, v)), - _ => None, - }) - .filter(|(k, _)| index.contains(&k.to_ascii_lowercase()) ^ invert) - .fold(vec![], |mut acc, (k, v)| { - acc.extend(*k); - acc.extend(b": "); - acc.extend(*v); - acc.extend(b"\r\n"); - acc - }); - - Ok(ExtractedFull(buffer.into())) - } - - /// The HEADER [...] part specifiers refer to the [RFC-2822] header of the message or of - /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. - /// ```raw - /// HEADER ([RFC-2822] header of the message) - /// ``` - fn header(&self) -> Result<ExtractedFull<'a>> { - let msg = self - .0 - .as_message() - .ok_or(anyhow!("Selected part must be a message/rfc822"))?; - Ok(ExtractedFull(msg.raw_headers.into())) - } - - /// The TEXT part specifier refers to the text body of the message, omitting the [RFC-2822] header. - fn text(&self) -> Result<ExtractedFull<'a>> { - let msg = self - .0 - .as_message() - .ok_or(anyhow!("Selected part must be a message/rfc822"))?; - Ok(ExtractedFull(msg.raw_body.into())) - } - - // ------------ - - /// Basic field of a MIME part that is - /// common to all parts - fn basic_fields(&self) -> Result<BasicFields<'static>> { - let sz = match self.0 { - AnyPart::Txt(x) => x.body.len(), - AnyPart::Bin(x) => x.body.len(), - AnyPart::Msg(x) => x.raw_part.len(), - AnyPart::Mult(_) => 0, - }; - let m = self.0.mime(); - let parameter_list = m - .ctype - .as_ref() - .map(|x| { - x.params - .iter() - .map(|p| { - ( - IString::try_from(String::from_utf8_lossy(p.name).to_string()), - IString::try_from(p.value.to_string()), - ) - }) - .filter(|(k, v)| k.is_ok() && v.is_ok()) - .map(|(k, v)| (k.unwrap(), v.unwrap())) - .collect() - }) - .unwrap_or(vec![]); - - Ok(BasicFields { - parameter_list, - id: NString( - m.id.as_ref() - .and_then(|ci| IString::try_from(ci.to_string()).ok()), - ), - description: NString( - m.description - .as_ref() - .and_then(|cd| IString::try_from(cd.to_string()).ok()), - ), - content_transfer_encoding: match m.transfer_encoding { - mime::mechanism::Mechanism::_8Bit => unchecked_istring("8bit"), - mime::mechanism::Mechanism::Binary => unchecked_istring("binary"), - mime::mechanism::Mechanism::QuotedPrintable => { - unchecked_istring("quoted-printable") - } - mime::mechanism::Mechanism::Base64 => unchecked_istring("base64"), - _ => unchecked_istring("7bit"), - }, - // @FIXME we can't compute the size of the message currently... - size: u32::try_from(sz)?, - }) - } -} - -// --------------------------- -struct NodeMsg<'a>(&'a NodeMime<'a>, &'a composite::Message<'a>); -impl<'a> NodeMsg<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let basic = SelectedMime(self.0 .0).basic_fields()?; - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Message { - envelope: Box::new(ImfView(&self.1.imf).message_envelope()), - body_structure: Box::new(NodeMime(&self.1.child).structure(is_ext)?), - number_of_lines: nol(self.1.raw_part), - }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} -struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>); -impl<'a> NodeMult<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let itype = &self.1.mime.interpreted_type; - let subtype = IString::try_from(itype.subtype.to_string()) - .unwrap_or(unchecked_istring("alternative")); - - let inner_bodies = self - .1 - .children - .iter() - .filter_map(|inner| NodeMime(&inner).structure(is_ext).ok()) - .collect::<Vec<_>>(); - - Vec1::validate(&inner_bodies)?; - let bodies = Vec1::unvalidated(inner_bodies); - - Ok(BodyStructure::Multi { - bodies, - subtype, - extension_data: match is_ext { - true => Some(MultiPartExtensionData { - parameter_list: vec![( - IString::try_from("boundary").unwrap(), - IString::try_from(self.1.mime.interpreted_type.boundary.to_string())?, - )], - tail: None, - }), - _ => None, - }, - }) - } -} -struct NodeTxt<'a>(&'a NodeMime<'a>, &'a discrete::Text<'a>); -impl<'a> NodeTxt<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let mut basic = SelectedMime(self.0 .0).basic_fields()?; - - // Get the interpreted content type, set it - let itype = match &self.1.mime.interpreted_type { - Deductible::Inferred(v) | Deductible::Explicit(v) => v, - }; - let subtype = - IString::try_from(itype.subtype.to_string()).unwrap_or(unchecked_istring("plain")); - - // Add charset to the list of parameters if we know it has been inferred as it will be - // missing from the parsed content. - if let Deductible::Inferred(charset) = &itype.charset { - basic.parameter_list.push(( - unchecked_istring("charset"), - IString::try_from(charset.to_string()).unwrap_or(unchecked_istring("us-ascii")), - )); - } - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Text { - subtype, - number_of_lines: nol(self.1.body), - }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} - -struct NodeBin<'a>(&'a NodeMime<'a>, &'a discrete::Binary<'a>); -impl<'a> NodeBin<'a> { - fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { - let basic = SelectedMime(self.0 .0).basic_fields()?; - - let default = mime::r#type::NaiveType { - main: &b"application"[..], - sub: &b"octet-stream"[..], - params: vec![], - }; - let ct = self.1.mime.fields.ctype.as_ref().unwrap_or(&default); - - let r#type = IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err( - anyhow!("Unable to build IString from given Content-Type type given"), - ))?; - - let subtype = IString::try_from(String::from_utf8_lossy(ct.sub).to_string()).or(Err( - anyhow!("Unable to build IString from given Content-Type subtype given"), - ))?; - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Basic { r#type, subtype }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} - -// --------------------------- - -struct ExtractedFull<'a>(Cow<'a, [u8]>); -impl<'a> ExtractedFull<'a> { - /// It is possible to fetch a substring of the designated text. - /// This is done by appending an open angle bracket ("<"), the - /// octet position of the first desired octet, a period, the - /// maximum number of octets desired, and a close angle bracket - /// (">") to the part specifier. If the starting octet is beyond - /// the end of the text, an empty string is returned. - /// - /// Any partial fetch that attempts to read beyond the end of the - /// text is truncated as appropriate. A partial fetch that starts - /// at octet 0 is returned as a partial fetch, even if this - /// truncation happened. - /// - /// Note: This means that BODY[]<0.2048> of a 1500-octet message - /// will return BODY[]<0> with a literal of size 1500, not - /// BODY[]. - /// - /// Note: A substring fetch of a HEADER.FIELDS or - /// HEADER.FIELDS.NOT part specifier is calculated after - /// subsetting the header. - fn to_body_section(self, partial: &'_ Option<(u32, NonZeroU32)>) -> BodySection<'a> { - match partial { - Some((begin, len)) => self.partialize(*begin, *len), - None => BodySection::Full(self.0), - } - } - - fn partialize(self, begin: u32, len: NonZeroU32) -> BodySection<'a> { - // Asked range is starting after the end of the content, - // returning an empty buffer - if begin as usize > self.0.len() { - return BodySection::Slice { - body: Cow::Borrowed(&[][..]), - origin_octet: begin, - }; - } - - // Asked range is ending after the end of the content, - // slice only the beginning of the buffer - if (begin + len.get()) as usize >= self.0.len() { - return BodySection::Slice { - body: match self.0 { - Cow::Borrowed(body) => Cow::Borrowed(&body[begin as usize..]), - Cow::Owned(body) => Cow::Owned(body[begin as usize..].to_vec()), - }, - origin_octet: begin, - }; - } - - // Range is included inside the considered content, - // this is the "happy case" - BodySection::Slice { - body: match self.0 { - Cow::Borrowed(body) => { - Cow::Borrowed(&body[begin as usize..(begin + len.get()) as usize]) - } - Cow::Owned(body) => { - Cow::Owned(body[begin as usize..(begin + len.get()) as usize].to_vec()) - } - }, - origin_octet: begin, - } - } -} - -/// ---- LEGACY - -/// s is set to static to ensure that only compile time values -/// checked by developpers are passed. -fn unchecked_istring(s: &'static str) -> IString { - IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") -} - -// Number Of Lines -fn nol(input: &[u8]) -> u32 { - input - .iter() - .filter(|x| **x == b'\n') - .count() - .try_into() - .unwrap_or(0) -} diff --git a/src/imap/mod.rs b/src/imap/mod.rs deleted file mode 100644 index 02ab9ce..0000000 --- a/src/imap/mod.rs +++ /dev/null @@ -1,421 +0,0 @@ -mod attributes; -mod capability; -mod command; -mod flags; -mod flow; -mod imf_view; -mod index; -mod mail_view; -mod mailbox_view; -mod mime_view; -mod request; -mod response; -mod search; -mod session; - -use std::net::SocketAddr; - -use anyhow::{anyhow, bail, Context, Result}; -use futures::stream::{FuturesUnordered, StreamExt}; - -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio::sync::watch; - -use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Response, Status}; -use imap_codec::imap_types::{core::Text, response::Greeting}; -use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; -use imap_flow::stream::AnyStream; -use rustls_pemfile::{certs, private_key}; -use tokio_rustls::TlsAcceptor; - -use crate::config::{ImapConfig, ImapUnsecureConfig}; -use crate::imap::capability::ServerCapability; -use crate::imap::request::Request; -use crate::imap::response::{Body, ResponseOrIdle}; -use crate::imap::session::Instance; -use crate::login::ArcLoginProvider; - -/// Server is a thin wrapper to register our Services in BàL -pub struct Server { - bind_addr: SocketAddr, - login_provider: ArcLoginProvider, - capabilities: ServerCapability, - tls: Option<TlsAcceptor>, -} - -#[derive(Clone)] -struct ClientContext { - addr: SocketAddr, - login_provider: ArcLoginProvider, - must_exit: watch::Receiver<bool>, - server_capabilities: ServerCapability, -} - -pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Result<Server> { - let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open( - config.certs, - )?)) - .collect::<Result<Vec<_>, _>>()?; - let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open( - config.key, - )?))? - .unwrap(); - - let tls_config = rustls::ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(loaded_certs, loaded_key)?; - let acceptor = TlsAcceptor::from(Arc::new(tls_config)); - - Ok(Server { - bind_addr: config.bind_addr, - login_provider: login, - capabilities: ServerCapability::default(), - tls: Some(acceptor), - }) -} - -pub fn new_unsecure(config: ImapUnsecureConfig, login: ArcLoginProvider) -> Server { - Server { - bind_addr: config.bind_addr, - login_provider: login, - capabilities: ServerCapability::default(), - tls: None, - } -} - -impl Server { - pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - tracing::info!("IMAP server listening on {:#}", self.bind_addr); - - let mut connections = FuturesUnordered::new(); - - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - let (socket, remote_addr) = tokio::select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - tracing::info!("IMAP: accepted connection from {}", remote_addr); - let stream = match self.tls.clone() { - Some(acceptor) => { - let stream = match acceptor.accept(socket).await { - Ok(v) => v, - Err(e) => { - tracing::error!(err=?e, "TLS negociation failed"); - continue; - } - }; - AnyStream::new(stream) - } - None => AnyStream::new(socket), - }; - - let client = ClientContext { - addr: remote_addr.clone(), - login_provider: self.login_provider.clone(), - must_exit: must_exit.clone(), - server_capabilities: self.capabilities.clone(), - }; - let conn = tokio::spawn(NetLoop::handler(client, stream)); - connections.push(conn); - } - drop(tcp); - - tracing::info!("IMAP server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -use std::sync::Arc; -use tokio::sync::mpsc::*; -use tokio::sync::Notify; -use tokio_util::bytes::BytesMut; - -const PIPELINABLE_COMMANDS: usize = 64; - -// @FIXME a full refactor of this part of the code will be needed sooner or later -struct NetLoop { - ctx: ClientContext, - server: ServerFlow, - cmd_tx: Sender<Request>, - resp_rx: UnboundedReceiver<ResponseOrIdle>, -} - -impl NetLoop { - async fn handler(ctx: ClientContext, sock: AnyStream) { - let addr = ctx.addr.clone(); - - let mut nl = match Self::new(ctx, sock).await { - Ok(nl) => { - tracing::debug!(addr=?addr, "netloop successfully initialized"); - nl - } - Err(e) => { - tracing::error!(addr=?addr, err=?e, "netloop can not be initialized, closing session"); - return; - } - }; - - match nl.core().await { - Ok(()) => { - tracing::debug!("closing successful netloop core for {:?}", addr); - } - Err(e) => { - tracing::error!("closing errored netloop core for {:?}: {}", addr, e); - } - } - } - - async fn new(ctx: ClientContext, sock: AnyStream) -> Result<Self> { - let mut opts = ServerFlowOptions::default(); - opts.crlf_relaxed = false; - opts.literal_accept_text = Text::unvalidated("OK"); - opts.literal_reject_text = Text::unvalidated("Literal rejected"); - - // Send greeting - let (server, _) = ServerFlow::send_greeting( - sock, - opts, - Greeting::ok( - Some(Code::Capability(ctx.server_capabilities.to_vec())), - "Aerogramme", - ) - .unwrap(), - ) - .await?; - - // Start a mailbox session in background - let (cmd_tx, cmd_rx) = mpsc::channel::<Request>(PIPELINABLE_COMMANDS); - let (resp_tx, resp_rx) = mpsc::unbounded_channel::<ResponseOrIdle>(); - tokio::spawn(Self::session(ctx.clone(), cmd_rx, resp_tx)); - - // Return the object - Ok(NetLoop { - ctx, - server, - cmd_tx, - resp_rx, - }) - } - - /// Coms with the background session - async fn session( - ctx: ClientContext, - mut cmd_rx: Receiver<Request>, - resp_tx: UnboundedSender<ResponseOrIdle>, - ) -> () { - let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities); - loop { - let cmd = match cmd_rx.recv().await { - None => break, - Some(cmd_recv) => cmd_recv, - }; - - tracing::debug!(cmd=?cmd, sock=%ctx.addr, "command"); - let maybe_response = session.request(cmd).await; - tracing::debug!(cmd=?maybe_response, sock=%ctx.addr, "response"); - - match resp_tx.send(maybe_response) { - Err(_) => break, - Ok(_) => (), - }; - } - tracing::info!("runner is quitting"); - } - - async fn core(&mut self) -> Result<()> { - let mut maybe_idle: Option<Arc<Notify>> = None; - loop { - tokio::select! { - // Managing imap_flow stuff - srv_evt = self.server.progress() => match srv_evt? { - ServerFlowEvent::ResponseSent { handle: _handle, response } => { - match response { - Response::Status(Status::Bye(_)) => return Ok(()), - _ => tracing::trace!("sent to {} content {:?}", self.ctx.addr, response), - } - }, - ServerFlowEvent::CommandReceived { command } => { - match self.cmd_tx.try_send(Request::ImapCommand(command)) { - Ok(_) => (), - Err(mpsc::error::TrySendError::Full(_)) => { - self.server.enqueue_status(Status::bye(None, "Too fast").unwrap()); - tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr); - } - _ => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - } - } - }, - ServerFlowEvent::IdleCommandReceived { tag } => { - match self.cmd_tx.try_send(Request::IdleStart(tag)) { - Ok(_) => (), - Err(mpsc::error::TrySendError::Full(_)) => { - self.server.enqueue_status(Status::bye(None, "Too fast").unwrap()); - tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr); - } - _ => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - } - } - } - ServerFlowEvent::IdleDoneReceived => { - tracing::trace!("client sent DONE and want to stop IDLE"); - maybe_idle.ok_or(anyhow!("Received IDLE done but not idling currently"))?.notify_one(); - maybe_idle = None; - } - flow => { - self.server.enqueue_status(Status::bye(None, "Unsupported server flow event").unwrap()); - tracing::error!("session task exited for {:?} due to unsupported flow {:?}", self.ctx.addr, flow); - } - }, - - // Managing response generated by Aerogramme - maybe_msg = self.resp_rx.recv() => match maybe_msg { - Some(ResponseOrIdle::Response(response)) => { - tracing::trace!("Interactive, server has a response for the client"); - for body_elem in response.body.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.server.enqueue_status(response.completion); - }, - Some(ResponseOrIdle::IdleAccept(stop)) => { - tracing::trace!("Interactive, server agreed to switch in idle mode"); - let cr = CommandContinuationRequest::basic(None, "Idling")?; - self.server.idle_accept(cr).or(Err(anyhow!("refused continuation for idle accept")))?; - self.cmd_tx.try_send(Request::IdlePoll)?; - if maybe_idle.is_some() { - bail!("Can't start IDLE if already idling"); - } - maybe_idle = Some(stop); - }, - Some(ResponseOrIdle::IdleEvent(elems)) => { - tracing::trace!("server imap session has some change to communicate to the client"); - for body_elem in elems.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.cmd_tx.try_send(Request::IdlePoll)?; - }, - Some(ResponseOrIdle::IdleReject(response)) => { - tracing::trace!("inform client that session rejected idle"); - self.server - .idle_reject(response.completion) - .or(Err(anyhow!("wrong reject command")))?; - }, - None => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - }, - Some(_) => unreachable!(), - - }, - - // When receiving a CTRL+C - _ = self.ctx.must_exit.changed() => { - tracing::trace!("Interactive, CTRL+C, exiting"); - self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap()); - }, - }; - } - } - - /* - async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc<Notify>) -> Result<LoopMode> { - // Flush send - loop { - tracing::trace!("flush server send"); - match self.server.progress_send().await? { - Some(..) => continue, - None => break, - } - } - - tokio::select! { - // Receiving IDLE event from background - maybe_msg = self.resp_rx.recv() => match maybe_msg { - // Session decided idle is terminated - Some(ResponseOrIdle::Response(response)) => { - tracing::trace!("server imap session said idle is done, sending response done, switching to interactive"); - for body_elem in response.body.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.server.enqueue_status(response.completion); - return Ok(LoopMode::Interactive) - }, - // Session has some information for user - Some(ResponseOrIdle::IdleEvent(elems)) => { - tracing::trace!("server imap session has some change to communicate to the client"); - for body_elem in elems.into_iter() { - let _handle = match body_elem { - Body::Data(d) => self.server.enqueue_data(d), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.cmd_tx.try_send(Request::Idle)?; - return Ok(LoopMode::Idle(buff, stop)) - }, - - // Session crashed - None => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - return Ok(LoopMode::Interactive) - }, - - // Session can't start idling while already idling, it's a logic error! - Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"), - }, - - // User is trying to interact with us - read_client_result = self.server.stream.read(&mut buff) => { - let _bytes_read = read_client_result?; - use imap_codec::decode::Decoder; - let codec = imap_codec::IdleDoneCodec::new(); - tracing::trace!("client sent some data for the server IMAP session"); - match codec.decode(&buff) { - Ok(([], imap_codec::imap_types::extensions::idle::IdleDone)) => { - // Session will be informed that it must stop idle - // It will generate the "done" message and change the loop mode - tracing::trace!("client sent DONE and want to stop IDLE"); - stop.notify_one() - }, - Err(_) => { - tracing::trace!("Unable to decode DONE, maybe not enough data were sent?"); - }, - _ => bail!("Client sent data after terminating the continuation without waiting for the server. This is an unsupported behavior and bug in Aerogramme, quitting."), - }; - - return Ok(LoopMode::Idle(buff, stop)) - }, - - // When receiving a CTRL+C - _ = self.ctx.must_exit.changed() => { - tracing::trace!("CTRL+C sent, aborting IDLE for this session"); - self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap()); - return Ok(LoopMode::Interactive) - }, - }; - }*/ -} diff --git a/src/imap/request.rs b/src/imap/request.rs deleted file mode 100644 index cff18a3..0000000 --- a/src/imap/request.rs +++ /dev/null @@ -1,9 +0,0 @@ -use imap_codec::imap_types::command::Command; -use imap_codec::imap_types::core::Tag; - -#[derive(Debug)] -pub enum Request { - ImapCommand(Command<'static>), - IdleStart(Tag<'static>), - IdlePoll, -} diff --git a/src/imap/response.rs b/src/imap/response.rs deleted file mode 100644 index b6a0e98..0000000 --- a/src/imap/response.rs +++ /dev/null @@ -1,124 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::command::Command; -use imap_codec::imap_types::core::Tag; -use imap_codec::imap_types::response::{Code, Data, Status}; -use std::sync::Arc; -use tokio::sync::Notify; - -#[derive(Debug)] -pub enum Body<'a> { - Data(Data<'a>), - Status(Status<'a>), -} - -pub struct ResponseBuilder<'a> { - tag: Option<Tag<'a>>, - code: Option<Code<'a>>, - text: String, - body: Vec<Body<'a>>, -} - -impl<'a> ResponseBuilder<'a> { - pub fn to_req(mut self, cmd: &Command<'a>) -> Self { - self.tag = Some(cmd.tag.clone()); - self - } - pub fn tag(mut self, tag: Tag<'a>) -> Self { - self.tag = Some(tag); - self - } - - pub fn message(mut self, txt: impl Into<String>) -> Self { - self.text = txt.into(); - self - } - - pub fn code(mut self, code: Code<'a>) -> Self { - self.code = Some(code); - self - } - - pub fn data(mut self, data: Data<'a>) -> Self { - self.body.push(Body::Data(data)); - self - } - - pub fn many_data(mut self, data: Vec<Data<'a>>) -> Self { - for d in data.into_iter() { - self = self.data(d); - } - self - } - - #[allow(dead_code)] - pub fn info(mut self, status: Status<'a>) -> Self { - self.body.push(Body::Status(status)); - self - } - - #[allow(dead_code)] - pub fn many_info(mut self, status: Vec<Status<'a>>) -> Self { - for d in status.into_iter() { - self = self.info(d); - } - self - } - - pub fn set_body(mut self, body: Vec<Body<'a>>) -> Self { - self.body = body; - self - } - - pub fn ok(self) -> Result<Response<'a>> { - Ok(Response { - completion: Status::ok(self.tag, self.code, self.text)?, - body: self.body, - }) - } - - pub fn no(self) -> Result<Response<'a>> { - Ok(Response { - completion: Status::no(self.tag, self.code, self.text)?, - body: self.body, - }) - } - - pub fn bad(self) -> Result<Response<'a>> { - Ok(Response { - completion: Status::bad(self.tag, self.code, self.text)?, - body: self.body, - }) - } -} - -#[derive(Debug)] -pub struct Response<'a> { - pub body: Vec<Body<'a>>, - pub completion: Status<'a>, -} - -impl<'a> Response<'a> { - pub fn build() -> ResponseBuilder<'a> { - ResponseBuilder { - tag: None, - code: None, - text: "".to_string(), - body: vec![], - } - } - - pub fn bye() -> Result<Response<'a>> { - Ok(Response { - completion: Status::bye(None, "bye")?, - body: vec![], - }) - } -} - -#[derive(Debug)] -pub enum ResponseOrIdle { - Response(Response<'static>), - IdleAccept(Arc<Notify>), - IdleReject(Response<'static>), - IdleEvent(Vec<Body<'static>>), -} diff --git a/src/imap/search.rs b/src/imap/search.rs deleted file mode 100644 index 37a7e9e..0000000 --- a/src/imap/search.rs +++ /dev/null @@ -1,477 +0,0 @@ -use std::num::{NonZeroU32, NonZeroU64}; - -use imap_codec::imap_types::core::Vec1; -use imap_codec::imap_types::search::{MetadataItemSearch, SearchKey}; -use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; - -use crate::imap::index::MailIndex; -use crate::imap::mail_view::MailView; -use crate::mail::query::QueryScope; - -pub enum SeqType { - Undefined, - NonUid, - Uid, -} -impl SeqType { - pub fn is_uid(&self) -> bool { - matches!(self, Self::Uid) - } -} - -pub struct Criteria<'a>(pub &'a SearchKey<'a>); -impl<'a> Criteria<'a> { - /// Returns a set of email identifiers that is greater or equal - /// to the set of emails to return - pub fn to_sequence_set(&self) -> (SequenceSet, SeqType) { - match self.0 { - SearchKey::All => (sequence_set_all(), SeqType::Undefined), - SearchKey::SequenceSet(seq_set) => (seq_set.clone(), SeqType::NonUid), - SearchKey::Uid(seq_set) => (seq_set.clone(), SeqType::Uid), - SearchKey::Not(_inner) => { - tracing::debug!( - "using NOT in a search request is slow: it selects all identifiers" - ); - (sequence_set_all(), SeqType::Undefined) - } - SearchKey::Or(left, right) => { - tracing::debug!("using OR in a search request is slow: no deduplication is done"); - let (base, base_seqtype) = Self(&left).to_sequence_set(); - let (ext, ext_seqtype) = Self(&right).to_sequence_set(); - - // Check if we have a UID/ID conflict in fetching: now we don't know how to handle them - match (base_seqtype, ext_seqtype) { - (SeqType::Uid, SeqType::NonUid) | (SeqType::NonUid, SeqType::Uid) => { - (sequence_set_all(), SeqType::Undefined) - } - (SeqType::Undefined, x) | (x, _) => { - let mut new_vec = base.0.into_inner(); - new_vec.extend_from_slice(ext.0.as_ref()); - let seq = SequenceSet( - Vec1::try_from(new_vec) - .expect("merging non empty vec lead to non empty vec"), - ); - (seq, x) - } - } - } - SearchKey::And(search_list) => { - tracing::debug!( - "using AND in a search request is slow: no intersection is performed" - ); - // As we perform no intersection, we don't care if we mix uid or id. - // We only keep the smallest range, being it ID or UID, depending of - // which one has the less items. This is an approximation as UID ranges - // can have holes while ID ones can't. - search_list - .as_ref() - .iter() - .map(|crit| Self(&crit).to_sequence_set()) - .min_by(|(x, _), (y, _)| { - let x_size = approx_sequence_set_size(x); - let y_size = approx_sequence_set_size(y); - x_size.cmp(&y_size) - }) - .unwrap_or((sequence_set_all(), SeqType::Undefined)) - } - _ => (sequence_set_all(), SeqType::Undefined), - } - } - - /// Not really clever as we can have cases where we filter out - /// the email before needing to inspect its meta. - /// But for now we are seeking the most basic/stupid algorithm. - pub fn query_scope(&self) -> QueryScope { - use SearchKey::*; - match self.0 { - // Combinators - And(and_list) => and_list - .as_ref() - .iter() - .fold(QueryScope::Index, |prev, sk| { - prev.union(&Criteria(sk).query_scope()) - }), - Not(inner) => Criteria(inner).query_scope(), - Or(left, right) => Criteria(left) - .query_scope() - .union(&Criteria(right).query_scope()), - All => QueryScope::Index, - - // IMF Headers - Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) - | Subject(_) | To(_) => QueryScope::Partial, - // Internal Date is also stored in MailMeta - Before(_) | On(_) | Since(_) => QueryScope::Partial, - // Message size is also stored in MailMeta - Larger(_) | Smaller(_) => QueryScope::Partial, - // Text and Body require that we fetch the full content! - Text(_) | Body(_) => QueryScope::Full, - - _ => QueryScope::Index, - } - } - - pub fn is_modseq(&self) -> bool { - use SearchKey::*; - match self.0 { - And(and_list) => and_list - .as_ref() - .iter() - .any(|child| Criteria(child).is_modseq()), - Or(left, right) => Criteria(left).is_modseq() || Criteria(right).is_modseq(), - Not(child) => Criteria(child).is_modseq(), - ModSeq { .. } => true, - _ => false, - } - } - - /// Returns emails that we now for sure we want to keep - /// but also a second list of emails we need to investigate further by - /// fetching some remote data - pub fn filter_on_idx<'b>( - &self, - midx_list: &[&'b MailIndex<'b>], - ) -> (Vec<&'b MailIndex<'b>>, Vec<&'b MailIndex<'b>>) { - let (p1, p2): (Vec<_>, Vec<_>) = midx_list - .iter() - .map(|x| (x, self.is_keep_on_idx(x))) - .filter(|(_midx, decision)| decision.is_keep()) - .map(|(midx, decision)| (*midx, decision)) - .partition(|(_midx, decision)| matches!(decision, PartialDecision::Keep)); - - let to_keep = p1.into_iter().map(|(v, _)| v).collect(); - let to_fetch = p2.into_iter().map(|(v, _)| v).collect(); - (to_keep, to_fetch) - } - - // ---- - - /// Here we are doing a partial filtering: we do not have access - /// to the headers or to the body, so every time we encounter a rule - /// based on them, we need to keep it. - /// - /// @TODO Could be optimized on a per-email basis by also returning the QueryScope - /// when more information is needed! - fn is_keep_on_idx(&self, midx: &MailIndex) -> PartialDecision { - use SearchKey::*; - match self.0 { - // Combinator logic - And(expr_list) => expr_list - .as_ref() - .iter() - .fold(PartialDecision::Keep, |acc, cur| { - acc.and(&Criteria(cur).is_keep_on_idx(midx)) - }), - Or(left, right) => { - let left_decision = Criteria(left).is_keep_on_idx(midx); - let right_decision = Criteria(right).is_keep_on_idx(midx); - left_decision.or(&right_decision) - } - Not(expr) => Criteria(expr).is_keep_on_idx(midx).not(), - All => PartialDecision::Keep, - - // Sequence logic - maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, midx).into(), - maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, midx).into(), - ModSeq { - metadata_item, - modseq, - } => is_keep_modseq(metadata_item, modseq, midx).into(), - - // All the stuff we can't evaluate yet - Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) - | Subject(_) | To(_) | Before(_) | On(_) | Since(_) | Larger(_) | Smaller(_) - | Text(_) | Body(_) => PartialDecision::Postpone, - - unknown => { - tracing::error!("Unknown filter {:?}", unknown); - PartialDecision::Discard - } - } - } - - /// @TODO we re-eveluate twice the same logic. The correct way would be, on each pass, - /// to simplify the searck query, by removing the elements that were already checked. - /// For example if we have AND(OR(seqid(X), body(Y)), body(X)), we can't keep for sure - /// the email, as body(x) might be false. So we need to check it. But as seqid(x) is true, - /// we could simplify the request to just body(x) and truncate the first OR. Today, we are - /// not doing that, and thus we reevaluate everything. - pub fn is_keep_on_query(&self, mail_view: &MailView) -> bool { - use SearchKey::*; - match self.0 { - // Combinator logic - And(expr_list) => expr_list - .as_ref() - .iter() - .all(|cur| Criteria(cur).is_keep_on_query(mail_view)), - Or(left, right) => { - Criteria(left).is_keep_on_query(mail_view) - || Criteria(right).is_keep_on_query(mail_view) - } - Not(expr) => !Criteria(expr).is_keep_on_query(mail_view), - All => true, - - //@FIXME Reevaluating our previous logic... - maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, &mail_view.in_idx), - maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, &mail_view.in_idx), - ModSeq { - metadata_item, - modseq, - } => is_keep_modseq(metadata_item, modseq, &mail_view.in_idx).into(), - - // Filter on mail meta - Before(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive < search_naive.as_ref(), - _ => false, - }, - On(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive == search_naive.as_ref(), - _ => false, - }, - Since(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive > search_naive.as_ref(), - _ => false, - }, - - // Message size is also stored in MailMeta - Larger(size_ref) => { - mail_view - .query_result - .metadata() - .expect("metadata were fetched") - .rfc822_size - > *size_ref as usize - } - Smaller(size_ref) => { - mail_view - .query_result - .metadata() - .expect("metadata were fetched") - .rfc822_size - < *size_ref as usize - } - - // Filter on well-known headers - Bcc(txt) => mail_view.is_header_contains_pattern(&b"bcc"[..], txt.as_ref()), - Cc(txt) => mail_view.is_header_contains_pattern(&b"cc"[..], txt.as_ref()), - From(txt) => mail_view.is_header_contains_pattern(&b"from"[..], txt.as_ref()), - Subject(txt) => mail_view.is_header_contains_pattern(&b"subject"[..], txt.as_ref()), - To(txt) => mail_view.is_header_contains_pattern(&b"to"[..], txt.as_ref()), - Header(hdr, txt) => mail_view.is_header_contains_pattern(hdr.as_ref(), txt.as_ref()), - - // Filter on Date header - SentBefore(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive < search_naive.as_ref()) - .unwrap_or(false), - SentOn(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive == search_naive.as_ref()) - .unwrap_or(false), - SentSince(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive > search_naive.as_ref()) - .unwrap_or(false), - - // Filter on the full content of the email - Text(txt) => mail_view - .content - .as_msg() - .map(|msg| { - msg.raw_part - .windows(txt.as_ref().len()) - .any(|win| win == txt.as_ref()) - }) - .unwrap_or(false), - Body(txt) => mail_view - .content - .as_msg() - .map(|msg| { - msg.raw_body - .windows(txt.as_ref().len()) - .any(|win| win == txt.as_ref()) - }) - .unwrap_or(false), - - unknown => { - tracing::error!("Unknown filter {:?}", unknown); - false - } - } - } -} - -// ---- Sequence things ---- -fn sequence_set_all() -> SequenceSet { - SequenceSet::from(Sequence::Range( - SeqOrUid::Value(NonZeroU32::MIN), - SeqOrUid::Asterisk, - )) -} - -// This is wrong as sequences can overlap -fn approx_sequence_set_size(seq_set: &SequenceSet) -> u64 { - seq_set.0.as_ref().iter().fold(0u64, |acc, seq| { - acc.saturating_add(approx_sequence_size(seq)) - }) -} - -// This is wrong as sequence UID can have holes, -// as we don't know the number of messages in the mailbox also -// we gave to guess -fn approx_sequence_size(seq: &Sequence) -> u64 { - match seq { - Sequence::Single(_) => 1, - Sequence::Range(SeqOrUid::Asterisk, _) | Sequence::Range(_, SeqOrUid::Asterisk) => u64::MAX, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - let x2 = x2.get() as i64; - let x1 = x1.get() as i64; - (x2 - x1).abs().try_into().unwrap_or(1) - } - } -} - -// --- Partial decision things ---- - -enum PartialDecision { - Keep, - Discard, - Postpone, -} -impl From<bool> for PartialDecision { - fn from(x: bool) -> Self { - match x { - true => PartialDecision::Keep, - _ => PartialDecision::Discard, - } - } -} -impl PartialDecision { - fn not(&self) -> Self { - match self { - Self::Keep => Self::Discard, - Self::Discard => Self::Keep, - Self::Postpone => Self::Postpone, - } - } - - fn or(&self, other: &Self) -> Self { - match (self, other) { - (Self::Keep, _) | (_, Self::Keep) => Self::Keep, - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, - (Self::Discard, Self::Discard) => Self::Discard, - } - } - - fn and(&self, other: &Self) -> Self { - match (self, other) { - (Self::Discard, _) | (_, Self::Discard) => Self::Discard, - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, - (Self::Keep, Self::Keep) => Self::Keep, - } - } - - fn is_keep(&self) -> bool { - !matches!(self, Self::Discard) - } -} - -// ----- Search Key things --- -fn is_sk_flag(sk: &SearchKey) -> bool { - use SearchKey::*; - match sk { - Answered | Deleted | Draft | Flagged | Keyword(..) | New | Old | Recent | Seen - | Unanswered | Undeleted | Undraft | Unflagged | Unkeyword(..) | Unseen => true, - _ => false, - } -} - -fn is_keep_flag(sk: &SearchKey, midx: &MailIndex) -> bool { - use SearchKey::*; - match sk { - Answered => midx.is_flag_set("\\Answered"), - Deleted => midx.is_flag_set("\\Deleted"), - Draft => midx.is_flag_set("\\Draft"), - Flagged => midx.is_flag_set("\\Flagged"), - Keyword(kw) => midx.is_flag_set(kw.inner()), - New => { - let is_recent = midx.is_flag_set("\\Recent"); - let is_seen = midx.is_flag_set("\\Seen"); - is_recent && !is_seen - } - Old => { - let is_recent = midx.is_flag_set("\\Recent"); - !is_recent - } - Recent => midx.is_flag_set("\\Recent"), - Seen => midx.is_flag_set("\\Seen"), - Unanswered => { - let is_answered = midx.is_flag_set("\\Recent"); - !is_answered - } - Undeleted => { - let is_deleted = midx.is_flag_set("\\Deleted"); - !is_deleted - } - Undraft => { - let is_draft = midx.is_flag_set("\\Draft"); - !is_draft - } - Unflagged => { - let is_flagged = midx.is_flag_set("\\Flagged"); - !is_flagged - } - Unkeyword(kw) => { - let is_keyword_set = midx.is_flag_set(kw.inner()); - !is_keyword_set - } - Unseen => { - let is_seen = midx.is_flag_set("\\Seen"); - !is_seen - } - - // Not flag logic - _ => unreachable!(), - } -} - -fn is_sk_seq(sk: &SearchKey) -> bool { - use SearchKey::*; - match sk { - SequenceSet(..) | Uid(..) => true, - _ => false, - } -} -fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool { - use SearchKey::*; - match sk { - SequenceSet(seq_set) => seq_set - .0 - .as_ref() - .iter() - .any(|seq| midx.is_in_sequence_i(seq)), - Uid(seq_set) => seq_set - .0 - .as_ref() - .iter() - .any(|seq| midx.is_in_sequence_uid(seq)), - _ => unreachable!(), - } -} - -fn is_keep_modseq( - filter: &Option<MetadataItemSearch>, - modseq: &NonZeroU64, - midx: &MailIndex, -) -> bool { - if filter.is_some() { - tracing::warn!(filter=?filter, "Ignoring search metadata filter as it's not supported yet"); - } - modseq <= &midx.modseq -} diff --git a/src/imap/session.rs b/src/imap/session.rs deleted file mode 100644 index fa3232a..0000000 --- a/src/imap/session.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anonymous, authenticated, selected}; -use crate::imap::flow; -use crate::imap::request::Request; -use crate::imap::response::{Response, ResponseOrIdle}; -use crate::login::ArcLoginProvider; -use anyhow::{anyhow, bail, Context, Result}; -use imap_codec::imap_types::{command::Command, core::Tag}; - -//----- -pub struct Instance { - pub login_provider: ArcLoginProvider, - pub server_capabilities: ServerCapability, - pub client_capabilities: ClientCapability, - pub state: flow::State, -} -impl Instance { - pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { - let client_cap = ClientCapability::new(&cap); - Self { - login_provider, - state: flow::State::NotAuthenticated, - server_capabilities: cap, - client_capabilities: client_cap, - } - } - - pub async fn request(&mut self, req: Request) -> ResponseOrIdle { - match req { - Request::IdleStart(tag) => self.idle_init(tag), - Request::IdlePoll => self.idle_poll().await, - Request::ImapCommand(cmd) => self.command(cmd).await, - } - } - - pub fn idle_init(&mut self, tag: Tag<'static>) -> ResponseOrIdle { - // Build transition - //@FIXME the notifier should be hidden inside the state and thus not part of the transition! - let transition = flow::Transition::Idle(tag.clone(), tokio::sync::Notify::new()); - - // Try to apply the transition and get the stop notifier - let maybe_stop = self - .state - .apply(transition) - .context("IDLE transition failed") - .and_then(|_| { - self.state - .notify() - .ok_or(anyhow!("IDLE state has no Notify object")) - }); - - // Build an appropriate response - match maybe_stop { - Ok(stop) => ResponseOrIdle::IdleAccept(stop), - Err(e) => { - tracing::error!(err=?e, "unable to init idle due to a transition error"); - //ResponseOrIdle::IdleReject(tag) - let no = Response::build() - .tag(tag) - .message( - "Internal error, processing command triggered an illegal IMAP state transition", - ) - .no() - .unwrap(); - ResponseOrIdle::IdleReject(no) - } - } - } - - pub async fn idle_poll(&mut self) -> ResponseOrIdle { - match self.idle_poll_happy().await { - Ok(r) => r, - Err(e) => { - tracing::error!(err=?e, "something bad happened in idle"); - ResponseOrIdle::Response(Response::bye().unwrap()) - } - } - } - - pub async fn idle_poll_happy(&mut self) -> Result<ResponseOrIdle> { - let (mbx, tag, stop) = match &mut self.state { - flow::State::Idle(_, ref mut mbx, _, tag, stop) => (mbx, tag.clone(), stop.clone()), - _ => bail!("Invalid session state, can't idle"), - }; - - tokio::select! { - _ = stop.notified() => { - self.state.apply(flow::Transition::UnIdle)?; - return Ok(ResponseOrIdle::Response(Response::build() - .tag(tag.clone()) - .message("IDLE completed") - .ok()?)) - }, - change = mbx.idle_sync() => { - tracing::debug!("idle event"); - return Ok(ResponseOrIdle::IdleEvent(change?)); - } - } - } - - pub async fn command(&mut self, cmd: Command<'static>) -> ResponseOrIdle { - // Command behavior is modulated by the state. - // To prevent state error, we handle the same command in separate code paths. - let (resp, tr) = match &mut self.state { - flow::State::NotAuthenticated => { - let ctx = anonymous::AnonymousContext { - req: &cmd, - login_provider: &self.login_provider, - server_capabilities: &self.server_capabilities, - }; - anonymous::dispatch(ctx).await - } - flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &cmd, - server_capabilities: &self.server_capabilities, - client_capabilities: &mut self.client_capabilities, - user, - }; - authenticated::dispatch(ctx).await - } - flow::State::Selected(ref user, ref mut mailbox, ref perm) => { - let ctx = selected::SelectedContext { - req: &cmd, - server_capabilities: &self.server_capabilities, - client_capabilities: &mut self.client_capabilities, - user, - mailbox, - perm, - }; - selected::dispatch(ctx).await - } - flow::State::Idle(..) => Err(anyhow!("can not receive command while idling")), - flow::State::Logout => Response::build() - .tag(cmd.tag.clone()) - .message("No commands are allowed in the LOGOUT state.") - .bad() - .map(|r| (r, flow::Transition::None)), - } - .unwrap_or_else(|err| { - tracing::error!("Command error {:?} occured while processing {:?}", err, cmd); - ( - Response::build() - .to_req(&cmd) - .message("Internal error while processing command") - .bad() - .unwrap(), - flow::Transition::None, - ) - }); - - if let Err(e) = self.state.apply(tr) { - tracing::error!( - "Transition error {:?} occured while processing on command {:?}", - e, - cmd - ); - return ResponseOrIdle::Response(Response::build() - .to_req(&cmd) - .message( - "Internal error, processing command triggered an illegal IMAP state transition", - ) - .bad() - .unwrap()); - } - ResponseOrIdle::Response(resp) - - /*match &self.state { - flow::State::Idle(_, _, _, _, n) => ResponseOrIdle::StartIdle(n.clone()), - _ => ResponseOrIdle::Response(resp), - }*/ - } -} diff --git a/src/k2v_util.rs b/src/k2v_util.rs deleted file mode 100644 index 3cd969b..0000000 --- a/src/k2v_util.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* -use anyhow::Result; -// ---- UTIL: function to wait for a value to have changed in K2V ---- - -pub async fn k2v_wait_value_changed( - k2v: &storage::RowStore, - key: &storage::RowRef, -) -> Result<CausalValue> { - loop { - if let Some(ct) = prev_ct { - match k2v.poll_item(pk, sk, ct.clone(), None).await? { - None => continue, - Some(cv) => return Ok(cv), - } - } else { - match k2v.read_item(pk, sk).await { - Err(k2v_client::Error::NotFound) => { - k2v.insert_item(pk, sk, vec![0u8], None).await?; - } - Err(e) => return Err(e.into()), - Ok(cv) => return Ok(cv), - } - } - } -} -*/ diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index f065478..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -#![feature(type_alias_impl_trait)] -#![feature(async_fn_in_trait)] -#![feature(async_closure)] -#![feature(trait_alias)] - -pub mod auth; -pub mod bayou; -pub mod config; -pub mod cryptoblob; -pub mod dav; -pub mod imap; -pub mod k2v_util; -pub mod lmtp; -pub mod login; -pub mod mail; -pub mod server; -pub mod storage; -pub mod timestamp; -pub mod user; diff --git a/src/lmtp.rs b/src/lmtp.rs deleted file mode 100644 index dcd4bcc..0000000 --- a/src/lmtp.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::net::SocketAddr; -use std::{pin::Pin, sync::Arc}; - -use anyhow::Result; -use async_trait::async_trait; -use duplexify::Duplex; -use futures::{io, AsyncRead, AsyncReadExt, AsyncWrite}; -use futures::{ - stream, - stream::{FuturesOrdered, FuturesUnordered}, - StreamExt, -}; -use log::*; -use tokio::net::TcpListener; -use tokio::select; -use tokio::sync::watch; -use tokio_util::compat::*; - -use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode}; -use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata}; - -use crate::config::*; -use crate::login::*; -use crate::mail::incoming::EncryptedMessage; - -pub struct LmtpServer { - bind_addr: SocketAddr, - hostname: String, - login_provider: Arc<dyn LoginProvider + Send + Sync>, -} - -impl LmtpServer { - pub fn new( - config: LmtpConfig, - login_provider: Arc<dyn LoginProvider + Send + Sync>, - ) -> Arc<Self> { - Arc::new(Self { - bind_addr: config.bind_addr, - hostname: config.hostname, - login_provider, - }) - } - - pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - info!("LMTP server listening on {:#}", self.bind_addr); - - let mut connections = FuturesUnordered::new(); - - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - let (socket, remote_addr) = select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - info!("LMTP: accepted connection from {}", remote_addr); - - let conn = tokio::spawn(smtp_server::interact( - socket.compat(), - smtp_server::IsAlreadyTls::No, - (), - self.clone(), - )); - - connections.push(conn); - } - drop(tcp); - - info!("LMTP server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -// ---- - -pub struct Message { - to: Vec<PublicCredentials>, -} - -#[async_trait] -impl Config for LmtpServer { - type Protocol = smtp_server::protocol::Lmtp; - - type ConnectionUserMeta = (); - type MailUserMeta = Message; - - fn hostname(&self, _conn_meta: &ConnectionMetadata<()>) -> &str { - &self.hostname - } - - async fn new_mail(&self, _conn_meta: &mut ConnectionMetadata<()>) -> Message { - Message { to: vec![] } - } - - async fn tls_accept<IO>( - &self, - _io: IO, - _conn_meta: &mut ConnectionMetadata<()>, - ) -> io::Result<Duplex<Pin<Box<dyn Send + AsyncRead>>, Pin<Box<dyn Send + AsyncWrite>>>> - where - IO: Send + AsyncRead + AsyncWrite, - { - Err(io::Error::new( - io::ErrorKind::InvalidInput, - "TLS not implemented for LMTP server", - )) - } - - async fn filter_from( - &self, - from: Option<Email>, - _meta: &mut MailMetadata<Message>, - _conn_meta: &mut ConnectionMetadata<()>, - ) -> Decision<Option<Email>> { - Decision::Accept { - reply: reply::okay_from().convert(), - res: from, - } - } - - async fn filter_to( - &self, - to: Email, - meta: &mut MailMetadata<Message>, - _conn_meta: &mut ConnectionMetadata<()>, - ) -> Decision<Email> { - let to_str = match to.hostname.as_ref() { - Some(h) => format!("{}@{}", to.localpart, h), - None => to.localpart.to_string(), - }; - match self.login_provider.public_login(&to_str).await { - Ok(creds) => { - meta.user.to.push(creds); - Decision::Accept { - reply: reply::okay_to().convert(), - res: to, - } - } - Err(e) => Decision::Reject { - reply: Reply { - code: ReplyCode::POLICY_REASON, - ecode: None, - text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], - }, - }, - } - } - - async fn handle_mail<'resp, R>( - &'resp self, - reader: &mut EscapedDataReader<'_, R>, - meta: MailMetadata<Message>, - _conn_meta: &'resp mut ConnectionMetadata<()>, - ) -> Pin<Box<dyn futures::Stream<Item = Decision<()>> + Send + 'resp>> - where - R: Send + Unpin + AsyncRead, - { - let err_response_stream = |meta: MailMetadata<Message>, msg: String| { - Box::pin( - stream::iter(meta.user.to.into_iter()).map(move |_| Decision::Reject { - reply: Reply { - code: ReplyCode::POLICY_REASON, - ecode: None, - text: vec![smtp_message::MaybeUtf8::Utf8(msg.clone())], - }, - }), - ) - }; - - let mut text = Vec::new(); - if let Err(e) = reader.read_to_end(&mut text).await { - return err_response_stream(meta, format!("io error: {}", e)); - } - reader.complete(); - let raw_size = text.len(); - - // Unescape email, shrink it also to remove last dot - let unesc_res = DataUnescaper::new(true).unescape(&mut text); - text.truncate(unesc_res.written); - tracing::debug!(prev_sz = raw_size, new_sz = text.len(), "unescaped"); - - let encrypted_message = match EncryptedMessage::new(text) { - Ok(x) => Arc::new(x), - Err(e) => return err_response_stream(meta, e.to_string()), - }; - - Box::pin( - meta.user - .to - .into_iter() - .map(move |creds| { - let encrypted_message = encrypted_message.clone(); - async move { - match encrypted_message.deliver_to(creds).await { - Ok(()) => Decision::Accept { - reply: reply::okay_mail().convert(), - res: (), - }, - Err(e) => Decision::Reject { - reply: Reply { - code: ReplyCode::POLICY_REASON, - ecode: None, - text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], - }, - }, - } - } - }) - .collect::<FuturesOrdered<_>>(), - ) - } -} diff --git a/src/login/demo_provider.rs b/src/login/demo_provider.rs deleted file mode 100644 index 11c7d54..0000000 --- a/src/login/demo_provider.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::login::*; -use crate::storage::*; - -pub struct DemoLoginProvider { - keys: CryptoKeys, - in_memory_store: in_memory::MemDb, -} - -impl DemoLoginProvider { - pub fn new() -> Self { - Self { - keys: CryptoKeys::init(), - in_memory_store: in_memory::MemDb::new(), - } - } -} - -#[async_trait] -impl LoginProvider for DemoLoginProvider { - async fn login(&self, username: &str, password: &str) -> Result<Credentials> { - tracing::debug!(user=%username, "login"); - - if username != "alice" { - bail!("user does not exist"); - } - - if password != "hunter2" { - bail!("wrong password"); - } - - let storage = self.in_memory_store.builder("alice").await; - let keys = self.keys.clone(); - - Ok(Credentials { storage, keys }) - } - - async fn public_login(&self, email: &str) -> Result<PublicCredentials> { - tracing::debug!(user=%email, "public_login"); - if email != "alice@example.tld" { - bail!("invalid email address"); - } - - let storage = self.in_memory_store.builder("alice").await; - let public_key = self.keys.public.clone(); - - Ok(PublicCredentials { - storage, - public_key, - }) - } -} diff --git a/src/login/ldap_provider.rs b/src/login/ldap_provider.rs deleted file mode 100644 index 0af5676..0000000 --- a/src/login/ldap_provider.rs +++ /dev/null @@ -1,265 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use ldap3::{LdapConnAsync, Scope, SearchEntry}; -use log::debug; - -use crate::config::*; -use crate::login::*; -use crate::storage; - -pub struct LdapLoginProvider { - ldap_server: String, - - pre_bind_on_login: bool, - bind_dn_and_pw: Option<(String, String)>, - - search_base: String, - attrs_to_retrieve: Vec<String>, - username_attr: String, - mail_attr: String, - crypto_root_attr: String, - - storage_specific: StorageSpecific, - in_memory_store: storage::in_memory::MemDb, - garage_store: storage::garage::GarageRoot, -} - -enum BucketSource { - Constant(String), - Attr(String), -} - -enum StorageSpecific { - InMemory, - Garage { - from_config: LdapGarageConfig, - bucket_source: BucketSource, - }, -} - -impl LdapLoginProvider { - pub fn new(config: LoginLdapConfig) -> Result<Self> { - let bind_dn_and_pw = match (config.bind_dn, config.bind_password) { - (Some(dn), Some(pw)) => Some((dn, pw)), - (None, None) => None, - _ => bail!( - "If either of `bind_dn` or `bind_password` is set, the other must be set as well." - ), - }; - - if config.pre_bind_on_login && bind_dn_and_pw.is_none() { - bail!("Cannot use `pre_bind_on_login` without setting `bind_dn` and `bind_password`"); - } - - let mut attrs_to_retrieve = vec![ - config.username_attr.clone(), - config.mail_attr.clone(), - config.crypto_root_attr.clone(), - ]; - - // storage specific - let specific = match config.storage { - LdapStorage::InMemory => StorageSpecific::InMemory, - LdapStorage::Garage(grgconf) => { - attrs_to_retrieve.push(grgconf.aws_access_key_id_attr.clone()); - attrs_to_retrieve.push(grgconf.aws_secret_access_key_attr.clone()); - - let bucket_source = - match (grgconf.default_bucket.clone(), grgconf.bucket_attr.clone()) { - (Some(b), None) => BucketSource::Constant(b), - (None, Some(a)) => BucketSource::Attr(a), - _ => bail!("Must set `bucket` or `bucket_attr`, but not both"), - }; - - if let BucketSource::Attr(a) = &bucket_source { - attrs_to_retrieve.push(a.clone()); - } - - StorageSpecific::Garage { - from_config: grgconf, - bucket_source, - } - } - }; - - Ok(Self { - ldap_server: config.ldap_server, - pre_bind_on_login: config.pre_bind_on_login, - bind_dn_and_pw, - search_base: config.search_base, - attrs_to_retrieve, - username_attr: config.username_attr, - mail_attr: config.mail_attr, - crypto_root_attr: config.crypto_root_attr, - storage_specific: specific, - //@FIXME should be created outside of the login provider - //Login provider should return only a cryptoroot + a storage URI - //storage URI that should be resolved outside... - in_memory_store: storage::in_memory::MemDb::new(), - garage_store: storage::garage::GarageRoot::new()?, - }) - } - - async fn storage_creds_from_ldap_user(&self, user: &SearchEntry) -> Result<Builder> { - let storage: Builder = match &self.storage_specific { - StorageSpecific::InMemory => { - self.in_memory_store - .builder(&get_attr(user, &self.username_attr)?) - .await - } - StorageSpecific::Garage { - from_config, - bucket_source, - } => { - let aws_access_key_id = get_attr(user, &from_config.aws_access_key_id_attr)?; - let aws_secret_access_key = - get_attr(user, &from_config.aws_secret_access_key_attr)?; - let bucket = match bucket_source { - BucketSource::Constant(b) => b.clone(), - BucketSource::Attr(a) => get_attr(user, &a)?, - }; - - self.garage_store.user(storage::garage::GarageConf { - region: from_config.aws_region.clone(), - s3_endpoint: from_config.s3_endpoint.clone(), - k2v_endpoint: from_config.k2v_endpoint.clone(), - aws_access_key_id, - aws_secret_access_key, - bucket, - })? - } - }; - - Ok(storage) - } -} - -#[async_trait] -impl LoginProvider for LdapLoginProvider { - async fn login(&self, username: &str, password: &str) -> Result<Credentials> { - check_identifier(username)?; - - let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?; - ldap3::drive!(conn); - - if self.pre_bind_on_login { - let (dn, pw) = self.bind_dn_and_pw.as_ref().unwrap(); - ldap.simple_bind(dn, pw).await?.success()?; - } - - let (matches, _res) = ldap - .search( - &self.search_base, - Scope::Subtree, - &format!( - "(&(objectClass=inetOrgPerson)({}={}))", - self.username_attr, username - ), - &self.attrs_to_retrieve, - ) - .await? - .success()?; - - if matches.is_empty() { - bail!("Invalid username"); - } - if matches.len() > 1 { - bail!("Invalid username (multiple matching accounts)"); - } - let user = SearchEntry::construct(matches.into_iter().next().unwrap()); - debug!( - "Found matching LDAP user for username {}: {}", - username, user.dn - ); - - // Try to login against LDAP server with provided password - // to check user's password - ldap.simple_bind(&user.dn, password) - .await? - .success() - .context("Invalid password")?; - debug!("Ldap login with user name {} successfull", username); - - // cryptography - let crstr = get_attr(&user, &self.crypto_root_attr)?; - let cr = CryptoRoot(crstr); - let keys = cr.crypto_keys(password)?; - - // storage - let storage = self.storage_creds_from_ldap_user(&user).await?; - - drop(ldap); - - Ok(Credentials { storage, keys }) - } - - async fn public_login(&self, email: &str) -> Result<PublicCredentials> { - check_identifier(email)?; - - let (dn, pw) = match self.bind_dn_and_pw.as_ref() { - Some(x) => x, - None => bail!("Missing bind_dn and bind_password in LDAP login provider config"), - }; - - let (conn, mut ldap) = LdapConnAsync::new(&self.ldap_server).await?; - ldap3::drive!(conn); - ldap.simple_bind(dn, pw).await?.success()?; - - let (matches, _res) = ldap - .search( - &self.search_base, - Scope::Subtree, - &format!( - "(&(objectClass=inetOrgPerson)({}={}))", - self.mail_attr, email - ), - &self.attrs_to_retrieve, - ) - .await? - .success()?; - - if matches.is_empty() { - bail!("No such user account"); - } - if matches.len() > 1 { - bail!("Multiple matching user accounts"); - } - let user = SearchEntry::construct(matches.into_iter().next().unwrap()); - debug!("Found matching LDAP user for email {}: {}", email, user.dn); - - // cryptography - let crstr = get_attr(&user, &self.crypto_root_attr)?; - let cr = CryptoRoot(crstr); - let public_key = cr.public_key()?; - - // storage - let storage = self.storage_creds_from_ldap_user(&user).await?; - drop(ldap); - - Ok(PublicCredentials { - storage, - public_key, - }) - } -} - -fn get_attr(user: &SearchEntry, attr: &str) -> Result<String> { - Ok(user - .attrs - .get(attr) - .ok_or(anyhow!("Missing attr: {}", attr))? - .iter() - .next() - .ok_or(anyhow!("No value for attr: {}", attr))? - .clone()) -} - -fn check_identifier(id: &str) -> Result<()> { - let is_ok = id - .chars() - .all(|c| c.is_alphanumeric() || "-+_.@".contains(c)); - if !is_ok { - bail!("Invalid username/email address, must contain only a-z A-Z 0-9 - + _ . @"); - } - Ok(()) -} diff --git a/src/login/mod.rs b/src/login/mod.rs deleted file mode 100644 index 4a1dee1..0000000 --- a/src/login/mod.rs +++ /dev/null @@ -1,245 +0,0 @@ -pub mod demo_provider; -pub mod ldap_provider; -pub mod static_provider; - -use base64::Engine; -use std::sync::Arc; - -use anyhow::{anyhow, bail, Context, Result}; -use async_trait::async_trait; -use rand::prelude::*; - -use crate::cryptoblob::*; -use crate::storage::*; - -/// The trait LoginProvider defines the interface for a login provider that allows -/// to retrieve storage and cryptographic credentials for access to a user account -/// from their username and password. -#[async_trait] -pub trait LoginProvider { - /// The login method takes an account's password as an input to decypher - /// decryption keys and obtain full access to the user's account. - async fn login(&self, username: &str, password: &str) -> Result<Credentials>; - /// The public_login method takes an account's email address and returns - /// public credentials for adding mails to the user's inbox. - async fn public_login(&self, email: &str) -> Result<PublicCredentials>; -} - -/// ArcLoginProvider is simply an alias on a structure that is used -/// in many places in the code -pub type ArcLoginProvider = Arc<dyn LoginProvider + Send + Sync>; - -/// The struct Credentials represent all of the necessary information to interact -/// with a user account's data after they are logged in. -#[derive(Clone, Debug)] -pub struct Credentials { - /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V) - pub storage: Builder, - /// The cryptographic keys are used to encrypt and decrypt data stored in S3 and K2V - pub keys: CryptoKeys, -} - -#[derive(Clone, Debug)] -pub struct PublicCredentials { - /// The storage credentials are used to authenticate access to the underlying storage (S3, K2V) - pub storage: Builder, - pub public_key: PublicKey, -} - -use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CryptoRoot(pub String); - -impl CryptoRoot { - pub fn create_pass(password: &str, k: &CryptoKeys) -> Result<Self> { - let bytes = k.password_seal(password)?; - let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); - let cr = format!("aero:cryptoroot:pass:{}", b64); - Ok(Self(cr)) - } - - pub fn create_cleartext(k: &CryptoKeys) -> Self { - let bytes = k.serialize(); - let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); - let cr = format!("aero:cryptoroot:cleartext:{}", b64); - Self(cr) - } - - pub fn create_incoming(pk: &PublicKey) -> Self { - let bytes: &[u8] = &pk[..]; - let b64 = base64::engine::general_purpose::STANDARD_NO_PAD.encode(bytes); - let cr = format!("aero:cryptoroot:incoming:{}", b64); - Self(cr) - } - - pub fn public_key(&self) -> Result<PublicKey> { - match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] { - ["aero", "cryptoroot", "pass", b64blob] => { - let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; - if blob.len() < 32 { - bail!( - "Decoded data is {} bytes long, expect at least 32 bytes", - blob.len() - ); - } - PublicKey::from_slice(&blob[..32]).context("must be a valid public key") - } - ["aero", "cryptoroot", "cleartext", b64blob] => { - let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; - Ok(CryptoKeys::deserialize(&blob)?.public) - } - ["aero", "cryptoroot", "incoming", b64blob] => { - let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; - if blob.len() < 32 { - bail!( - "Decoded data is {} bytes long, expect at least 32 bytes", - blob.len() - ); - } - PublicKey::from_slice(&blob[..32]).context("must be a valid public key") - } - ["aero", "cryptoroot", "keyring", _] => { - bail!("keyring is not yet implemented!") - } - _ => bail!(format!( - "passed string '{}' is not a valid cryptoroot", - self.0 - )), - } - } - pub fn crypto_keys(&self, password: &str) -> Result<CryptoKeys> { - match self.0.splitn(4, ':').collect::<Vec<&str>>()[..] { - ["aero", "cryptoroot", "pass", b64blob] => { - let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; - CryptoKeys::password_open(password, &blob) - } - ["aero", "cryptoroot", "cleartext", b64blob] => { - let blob = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64blob)?; - CryptoKeys::deserialize(&blob) - } - ["aero", "cryptoroot", "incoming", _] => { - bail!("incoming cryptoroot does not contain a crypto key!") - } - ["aero", "cryptoroot", "keyring", _] => { - bail!("keyring is not yet implemented!") - } - _ => bail!(format!( - "passed string '{}' is not a valid cryptoroot", - self.0 - )), - } - } -} - -/// The struct CryptoKeys contains the cryptographic keys used to encrypt and decrypt -/// data in a user's mailbox. -#[derive(Clone, Debug)] -pub struct CryptoKeys { - /// Master key for symmetric encryption of mailbox data - pub master: Key, - /// Public/private keypair for encryption of incomming emails (secret part) - pub secret: SecretKey, - /// Public/private keypair for encryption of incomming emails (public part) - pub public: PublicKey, -} - -// ---- - -impl CryptoKeys { - /// Initialize a new cryptography root - pub fn init() -> Self { - let (public, secret) = gen_keypair(); - let master = gen_key(); - CryptoKeys { - master, - secret, - public, - } - } - - // Clear text serialize/deserialize - /// Serialize the root as bytes without encryption - fn serialize(&self) -> [u8; 64] { - let mut res = [0u8; 64]; - res[..32].copy_from_slice(self.master.as_ref()); - res[32..].copy_from_slice(self.secret.as_ref()); - res - } - - /// Deserialize a clear text crypto root without encryption - fn deserialize(bytes: &[u8]) -> Result<Self> { - if bytes.len() != 64 { - bail!("Invalid length: {}, expected 64", bytes.len()); - } - let master = Key::from_slice(&bytes[..32]).unwrap(); - let secret = SecretKey::from_slice(&bytes[32..]).unwrap(); - let public = secret.public_key(); - Ok(Self { - master, - secret, - public, - }) - } - - // Password sealed keys serialize/deserialize - pub fn password_open(password: &str, blob: &[u8]) -> Result<Self> { - let _pubkey = &blob[0..32]; - let kdf_salt = &blob[32..64]; - let password_openned = try_open_encrypted_keys(kdf_salt, password, &blob[64..])?; - - let keys = Self::deserialize(&password_openned)?; - Ok(keys) - } - - pub fn password_seal(&self, password: &str) -> Result<Vec<u8>> { - let mut kdf_salt = [0u8; 32]; - thread_rng().fill(&mut kdf_salt); - - // Calculate key for password secret box - let password_key = derive_password_key(&kdf_salt, password)?; - - // Seal a secret box that contains our crypto keys - let password_sealed = seal(&self.serialize(), &password_key)?; - - // Create blob - let password_blob = [&self.public[..], &kdf_salt[..], &password_sealed].concat(); - - Ok(password_blob) - } -} - -fn derive_password_key(kdf_salt: &[u8], password: &str) -> Result<Key> { - Ok(Key::from_slice(&argon2_kdf(kdf_salt, password.as_bytes(), 32)?).unwrap()) -} - -fn try_open_encrypted_keys( - kdf_salt: &[u8], - password: &str, - encrypted_keys: &[u8], -) -> Result<Vec<u8>> { - let password_key = derive_password_key(kdf_salt, password)?; - open(encrypted_keys, &password_key) -} - -// ---- UTIL ---- - -pub fn argon2_kdf(salt: &[u8], password: &[u8], output_len: usize) -> Result<Vec<u8>> { - use argon2::{password_hash, Algorithm, Argon2, ParamsBuilder, PasswordHasher, Version}; - - let params = ParamsBuilder::new() - .output_len(output_len) - .build() - .map_err(|e| anyhow!("Invalid argon2 params: {}", e))?; - let argon2 = Argon2::new(Algorithm::default(), Version::default(), params); - - let b64_salt = base64::engine::general_purpose::STANDARD_NO_PAD.encode(salt); - let valid_salt = password_hash::Salt::from_b64(&b64_salt) - .map_err(|e| anyhow!("Invalid salt, error {}", e))?; - let hash = argon2 - .hash_password(password, valid_salt) - .map_err(|e| anyhow!("Unable to hash: {}", e))?; - - let hash = hash.hash.ok_or(anyhow!("Missing output"))?; - assert!(hash.len() == output_len); - Ok(hash.as_bytes().to_vec()) -} diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs deleted file mode 100644 index 79626df..0000000 --- a/src/login/static_provider.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::signal::unix::{signal, SignalKind}; -use tokio::sync::watch; - -use anyhow::{anyhow, bail, Result}; -use async_trait::async_trait; - -use crate::config::*; -use crate::login::*; -use crate::storage; - -pub struct ContextualUserEntry { - pub username: String, - pub config: UserEntry, -} - -#[derive(Default)] -pub struct UserDatabase { - users: HashMap<String, Arc<ContextualUserEntry>>, - users_by_email: HashMap<String, Arc<ContextualUserEntry>>, -} - -pub struct StaticLoginProvider { - user_db: watch::Receiver<UserDatabase>, - in_memory_store: storage::in_memory::MemDb, - garage_store: storage::garage::GarageRoot, -} - -pub async fn update_user_list(config: PathBuf, up: watch::Sender<UserDatabase>) -> Result<()> { - let mut stream = signal(SignalKind::user_defined1()) - .expect("failed to install SIGUSR1 signal hander for reload"); - - loop { - let ulist: UserList = match read_config(config.clone()) { - Ok(x) => x, - Err(e) => { - tracing::warn!(path=%config.as_path().to_string_lossy(), error=%e, "Unable to load config"); - stream.recv().await; - continue; - } - }; - - let users = ulist - .into_iter() - .map(|(username, config)| { - ( - username.clone(), - Arc::new(ContextualUserEntry { username, config }), - ) - }) - .collect::<HashMap<_, _>>(); - - let mut users_by_email = HashMap::new(); - for (_, u) in users.iter() { - for m in u.config.email_addresses.iter() { - if users_by_email.contains_key(m) { - tracing::warn!("Several users have the same email address: {}", m); - stream.recv().await; - continue; - } - users_by_email.insert(m.clone(), u.clone()); - } - } - - tracing::info!("{} users loaded", users.len()); - up.send(UserDatabase { - users, - users_by_email, - }) - .context("update user db config")?; - stream.recv().await; - tracing::info!("Received SIGUSR1, reloading"); - } -} - -impl StaticLoginProvider { - pub async fn new(config: LoginStaticConfig) -> Result<Self> { - let (tx, mut rx) = watch::channel(UserDatabase::default()); - - tokio::spawn(update_user_list(config.user_list, tx)); - rx.changed().await?; - - Ok(Self { - user_db: rx, - in_memory_store: storage::in_memory::MemDb::new(), - garage_store: storage::garage::GarageRoot::new()?, - }) - } -} - -#[async_trait] -impl LoginProvider for StaticLoginProvider { - async fn login(&self, username: &str, password: &str) -> Result<Credentials> { - tracing::debug!(user=%username, "login"); - let user = { - let user_db = self.user_db.borrow(); - match user_db.users.get(username) { - None => bail!("User {} does not exist", username), - Some(u) => u.clone(), - } - }; - - tracing::debug!(user=%username, "verify password"); - if !verify_password(password, &user.config.password)? { - bail!("Wrong password"); - } - - tracing::debug!(user=%username, "fetch keys"); - let storage: storage::Builder = match &user.config.storage { - StaticStorage::InMemory => self.in_memory_store.builder(username).await, - StaticStorage::Garage(grgconf) => { - self.garage_store.user(storage::garage::GarageConf { - region: grgconf.aws_region.clone(), - k2v_endpoint: grgconf.k2v_endpoint.clone(), - s3_endpoint: grgconf.s3_endpoint.clone(), - aws_access_key_id: grgconf.aws_access_key_id.clone(), - aws_secret_access_key: grgconf.aws_secret_access_key.clone(), - bucket: grgconf.bucket.clone(), - })? - } - }; - - let cr = CryptoRoot(user.config.crypto_root.clone()); - let keys = cr.crypto_keys(password)?; - - tracing::debug!(user=%username, "logged"); - Ok(Credentials { storage, keys }) - } - - async fn public_login(&self, email: &str) -> Result<PublicCredentials> { - let user = { - let user_db = self.user_db.borrow(); - match user_db.users_by_email.get(email) { - None => bail!("Email {} does not exist", email), - Some(u) => u.clone(), - } - }; - tracing::debug!(user=%user.username, "public_login"); - - let storage: storage::Builder = match &user.config.storage { - StaticStorage::InMemory => self.in_memory_store.builder(&user.username).await, - StaticStorage::Garage(grgconf) => { - self.garage_store.user(storage::garage::GarageConf { - region: grgconf.aws_region.clone(), - k2v_endpoint: grgconf.k2v_endpoint.clone(), - s3_endpoint: grgconf.s3_endpoint.clone(), - aws_access_key_id: grgconf.aws_access_key_id.clone(), - aws_secret_access_key: grgconf.aws_secret_access_key.clone(), - bucket: grgconf.bucket.clone(), - })? - } - }; - - let cr = CryptoRoot(user.config.crypto_root.clone()); - let public_key = cr.public_key()?; - - Ok(PublicCredentials { - storage, - public_key, - }) - } -} - -pub fn hash_password(password: &str) -> Result<String> { - use argon2::{ - password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, - Argon2, - }; - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - Ok(argon2 - .hash_password(password.as_bytes(), &salt) - .map_err(|e| anyhow!("Argon2 error: {}", e))? - .to_string()) -} - -pub fn verify_password(password: &str, hash: &str) -> Result<bool> { - use argon2::{ - password_hash::{PasswordHash, PasswordVerifier}, - Argon2, - }; - let parsed_hash = - PasswordHash::new(hash).map_err(|e| anyhow!("Invalid hashed password: {}", e))?; - Ok(Argon2::default() - .verify_password(password.as_bytes(), &parsed_hash) - .is_ok()) -} diff --git a/src/mail/incoming.rs b/src/mail/incoming.rs deleted file mode 100644 index e2ad97d..0000000 --- a/src/mail/incoming.rs +++ /dev/null @@ -1,445 +0,0 @@ -//use std::collections::HashMap; -use std::convert::TryFrom; - -use std::sync::{Arc, Weak}; -use std::time::Duration; - -use anyhow::{anyhow, bail, Result}; -use base64::Engine; -use futures::{future::BoxFuture, FutureExt}; -//use tokio::io::AsyncReadExt; -use tokio::sync::watch; -use tracing::{debug, error, info, warn}; - -use crate::cryptoblob; -use crate::login::{Credentials, PublicCredentials}; -use crate::mail::mailbox::Mailbox; -use crate::mail::uidindex::ImapUidvalidity; -use crate::mail::unique_ident::*; -use crate::user::User; -use crate::mail::IMF; -use crate::storage; -use crate::timestamp::now_msec; - -const INCOMING_PK: &str = "incoming"; -const INCOMING_LOCK_SK: &str = "lock"; -const INCOMING_WATCH_SK: &str = "watch"; - -const MESSAGE_KEY: &str = "message-key"; - -// When a lock is held, it is held for LOCK_DURATION (here 5 minutes) -// It is renewed every LOCK_DURATION/3 -// If we are at 2*LOCK_DURATION/3 and haven't renewed, we assume we -// lost the lock. -const LOCK_DURATION: Duration = Duration::from_secs(300); - -// In addition to checking when notified, also check for new mail every 10 minutes -const MAIL_CHECK_INTERVAL: Duration = Duration::from_secs(600); - -pub async fn incoming_mail_watch_process( - user: Weak<User>, - creds: Credentials, - rx_inbox_id: watch::Receiver<Option<(UniqueIdent, ImapUidvalidity)>>, -) { - if let Err(e) = incoming_mail_watch_process_internal(user, creds, rx_inbox_id).await { - error!("Error in incoming mail watch process: {}", e); - } -} - -async fn incoming_mail_watch_process_internal( - user: Weak<User>, - creds: Credentials, - mut rx_inbox_id: watch::Receiver<Option<(UniqueIdent, ImapUidvalidity)>>, -) -> Result<()> { - let mut lock_held = k2v_lock_loop( - creds.storage.build().await?, - storage::RowRef::new(INCOMING_PK, INCOMING_LOCK_SK), - ); - let storage = creds.storage.build().await?; - - let mut inbox: Option<Arc<Mailbox>> = None; - let mut incoming_key = storage::RowRef::new(INCOMING_PK, INCOMING_WATCH_SK); - - loop { - let maybe_updated_incoming_key = if *lock_held.borrow() { - debug!("incoming lock held"); - - let wait_new_mail = async { - loop { - match storage.row_poll(&incoming_key).await { - Ok(row_val) => break row_val.row_ref, - Err(e) => { - error!("Error in wait_new_mail: {}", e); - tokio::time::sleep(Duration::from_secs(30)).await; - } - } - } - }; - - tokio::select! { - inc_k = wait_new_mail => Some(inc_k), - _ = tokio::time::sleep(MAIL_CHECK_INTERVAL) => Some(incoming_key.clone()), - _ = lock_held.changed() => None, - _ = rx_inbox_id.changed() => None, - } - } else { - debug!("incoming lock not held"); - tokio::select! { - _ = lock_held.changed() => None, - _ = rx_inbox_id.changed() => None, - } - }; - - let user = match Weak::upgrade(&user) { - Some(user) => user, - None => { - debug!("User no longer available, exiting incoming loop."); - break; - } - }; - debug!("User still available"); - - // If INBOX no longer is same mailbox, open new mailbox - let inbox_id = *rx_inbox_id.borrow(); - if let Some((id, uidvalidity)) = inbox_id { - if Some(id) != inbox.as_ref().map(|b| b.id) { - match user.open_mailbox_by_id(id, uidvalidity).await { - Ok(mb) => { - inbox = Some(mb); - } - Err(e) => { - inbox = None; - error!("Error when opening inbox ({}): {}", id, e); - tokio::time::sleep(Duration::from_secs(30)).await; - continue; - } - } - } - } - - // If we were able to open INBOX, and we have mail, - // fetch new mail - if let (Some(inbox), Some(updated_incoming_key)) = (&inbox, maybe_updated_incoming_key) { - match handle_incoming_mail(&user, &storage, inbox, &lock_held).await { - Ok(()) => { - incoming_key = updated_incoming_key; - } - Err(e) => { - error!("Could not fetch incoming mail: {}", e); - tokio::time::sleep(Duration::from_secs(30)).await; - } - } - } - } - drop(rx_inbox_id); - Ok(()) -} - -async fn handle_incoming_mail( - user: &Arc<User>, - storage: &storage::Store, - inbox: &Arc<Mailbox>, - lock_held: &watch::Receiver<bool>, -) -> Result<()> { - let mails_res = storage.blob_list("incoming/").await?; - - for object in mails_res { - if !*lock_held.borrow() { - break; - } - let key = object.0; - if let Some(mail_id) = key.strip_prefix("incoming/") { - if let Ok(mail_id) = mail_id.parse::<UniqueIdent>() { - move_incoming_message(user, storage, inbox, mail_id).await?; - } - } - } - - Ok(()) -} - -async fn move_incoming_message( - user: &Arc<User>, - storage: &storage::Store, - inbox: &Arc<Mailbox>, - id: UniqueIdent, -) -> Result<()> { - info!("Moving incoming message: {}", id); - - let object_key = format!("incoming/{}", id); - - // 1. Fetch message from S3 - let object = storage.blob_fetch(&storage::BlobRef(object_key)).await?; - - // 1.a decrypt message key from headers - //info!("Object metadata: {:?}", get_result.metadata); - let key_encrypted_b64 = object - .meta - .get(MESSAGE_KEY) - .ok_or(anyhow!("Missing key in metadata"))?; - let key_encrypted = base64::engine::general_purpose::STANDARD.decode(key_encrypted_b64)?; - let message_key = sodiumoxide::crypto::sealedbox::open( - &key_encrypted, - &user.creds.keys.public, - &user.creds.keys.secret, - ) - .map_err(|_| anyhow!("Cannot decrypt message key"))?; - let message_key = - cryptoblob::Key::from_slice(&message_key).ok_or(anyhow!("Invalid message key"))?; - - // 1.b retrieve message body - let obj_body = object.value; - let plain_mail = cryptoblob::open(&obj_body, &message_key) - .map_err(|_| anyhow!("Cannot decrypt email content"))?; - - // 2 parse mail and add to inbox - let msg = IMF::try_from(&plain_mail[..]).map_err(|_| anyhow!("Invalid email body"))?; - inbox - .append_from_s3(msg, id, object.blob_ref.clone(), message_key) - .await?; - - // 3 delete from incoming - storage.blob_rm(&object.blob_ref).await?; - - Ok(()) -} - -// ---- UTIL: K2V locking loop, use this to try to grab a lock using a K2V entry as a signal ---- - -fn k2v_lock_loop(storage: storage::Store, row_ref: storage::RowRef) -> watch::Receiver<bool> { - let (held_tx, held_rx) = watch::channel(false); - - tokio::spawn(k2v_lock_loop_internal(storage, row_ref, held_tx)); - - held_rx -} - -#[derive(Clone, Debug)] -enum LockState { - Unknown, - Empty, - Held(UniqueIdent, u64, storage::RowRef), -} - -async fn k2v_lock_loop_internal( - storage: storage::Store, - row_ref: storage::RowRef, - held_tx: watch::Sender<bool>, -) { - let (state_tx, mut state_rx) = watch::channel::<LockState>(LockState::Unknown); - let mut state_rx_2 = state_rx.clone(); - - let our_pid = gen_ident(); - - // Loop 1: watch state of lock in K2V, save that in corresponding watch channel - let watch_lock_loop: BoxFuture<Result<()>> = async { - let mut ct = row_ref.clone(); - loop { - debug!("k2v watch lock loop iter: ct = {:?}", ct); - match storage.row_poll(&ct).await { - Err(e) => { - error!( - "Error in k2v wait value changed: {} ; assuming we no longer hold lock.", - e - ); - state_tx.send(LockState::Unknown)?; - tokio::time::sleep(Duration::from_secs(30)).await; - } - Ok(cv) => { - let mut lock_state = None; - for v in cv.value.iter() { - if let storage::Alternative::Value(vbytes) = v { - if vbytes.len() == 32 { - let ts = u64::from_be_bytes(vbytes[..8].try_into().unwrap()); - let pid = UniqueIdent(vbytes[8..].try_into().unwrap()); - if lock_state - .map(|(pid2, ts2)| ts > ts2 || (ts == ts2 && pid > pid2)) - .unwrap_or(true) - { - lock_state = Some((pid, ts)); - } - } - } - } - let new_ct = cv.row_ref; - - debug!( - "k2v watch lock loop: changed, old ct = {:?}, new ct = {:?}, v = {:?}", - ct, new_ct, lock_state - ); - state_tx.send( - lock_state - .map(|(pid, ts)| LockState::Held(pid, ts, new_ct.clone())) - .unwrap_or(LockState::Empty), - )?; - ct = new_ct; - } - } - } - } - .boxed(); - - // Loop 2: notify user whether we are holding the lock or not - let lock_notify_loop: BoxFuture<Result<()>> = async { - loop { - let now = now_msec(); - let held_with_expiration_time = match &*state_rx.borrow_and_update() { - LockState::Held(pid, ts, _ct) if *pid == our_pid => { - let expiration_time = *ts - (LOCK_DURATION / 3).as_millis() as u64; - if now < expiration_time { - Some(expiration_time) - } else { - None - } - } - _ => None, - }; - let held = held_with_expiration_time.is_some(); - if held != *held_tx.borrow() { - held_tx.send(held)?; - } - - let await_expired = async { - match held_with_expiration_time { - None => futures::future::pending().await, - Some(expiration_time) => { - tokio::time::sleep(Duration::from_millis(expiration_time - now)).await - } - }; - }; - - tokio::select!( - r = state_rx.changed() => { - r?; - } - _ = held_tx.closed() => bail!("held_tx closed, don't need to hold lock anymore"), - _ = await_expired => continue, - ); - } - } - .boxed(); - - // Loop 3: acquire lock when relevant - let take_lock_loop: BoxFuture<Result<()>> = async { - loop { - let now = now_msec(); - let state: LockState = state_rx_2.borrow_and_update().clone(); - let (acquire_at, ct) = match state { - LockState::Unknown => { - // If state of the lock is unknown, don't try to acquire - state_rx_2.changed().await?; - continue; - } - LockState::Empty => (now, None), - LockState::Held(pid, ts, ct) => { - if pid == our_pid { - (ts - (2 * LOCK_DURATION / 3).as_millis() as u64, Some(ct)) - } else { - (ts, Some(ct)) - } - } - }; - - // Wait until it is time to acquire lock - if acquire_at > now { - tokio::select!( - r = state_rx_2.changed() => { - // If lock state changed in the meantime, don't acquire and loop around - r?; - continue; - } - _ = tokio::time::sleep(Duration::from_millis(acquire_at - now)) => () - ); - } - - // Acquire lock - let mut lock = vec![0u8; 32]; - lock[..8].copy_from_slice(&u64::to_be_bytes( - now_msec() + LOCK_DURATION.as_millis() as u64, - )); - lock[8..].copy_from_slice(&our_pid.0); - let row = match ct { - Some(existing) => existing, - None => row_ref.clone(), - }; - if let Err(e) = storage - .row_insert(vec![storage::RowVal::new(row, lock)]) - .await - { - error!("Could not take lock: {}", e); - tokio::time::sleep(Duration::from_secs(30)).await; - } - - // Wait for new information to loop back - state_rx_2.changed().await?; - } - } - .boxed(); - - let _ = futures::try_join!(watch_lock_loop, lock_notify_loop, take_lock_loop); - - debug!("lock loop exited, releasing"); - - if !held_tx.is_closed() { - warn!("weird..."); - let _ = held_tx.send(false); - } - - // If lock is ours, release it - let release = match &*state_rx.borrow() { - LockState::Held(pid, _, ct) if *pid == our_pid => Some(ct.clone()), - _ => None, - }; - if let Some(ct) = release { - match storage.row_rm(&storage::Selector::Single(&ct)).await { - Err(e) => warn!("Unable to release lock {:?}: {}", ct, e), - Ok(_) => (), - }; - } -} - -// ---- LMTP SIDE: storing messages encrypted with user's pubkey ---- - -pub struct EncryptedMessage { - key: cryptoblob::Key, - encrypted_body: Vec<u8>, -} - -impl EncryptedMessage { - pub fn new(body: Vec<u8>) -> Result<Self> { - let key = cryptoblob::gen_key(); - let encrypted_body = cryptoblob::seal(&body, &key)?; - Ok(Self { - key, - encrypted_body, - }) - } - - pub async fn deliver_to(self: Arc<Self>, creds: PublicCredentials) -> Result<()> { - let storage = creds.storage.build().await?; - - // Get causality token of previous watch key - let query = storage::RowRef::new(INCOMING_PK, INCOMING_WATCH_SK); - let watch_ct = match storage.row_fetch(&storage::Selector::Single(&query)).await { - Err(_) => query, - Ok(cv) => cv.into_iter().next().map(|v| v.row_ref).unwrap_or(query), - }; - - // Write mail to encrypted storage - let encrypted_key = - sodiumoxide::crypto::sealedbox::seal(self.key.as_ref(), &creds.public_key); - let key_header = base64::engine::general_purpose::STANDARD.encode(&encrypted_key); - - let blob_val = storage::BlobVal::new( - storage::BlobRef(format!("incoming/{}", gen_ident())), - self.encrypted_body.clone().into(), - ) - .with_meta(MESSAGE_KEY.to_string(), key_header); - storage.blob_insert(blob_val).await?; - - // Update watch key to signal new mail - let watch_val = storage::RowVal::new(watch_ct.clone(), gen_ident().0.to_vec()); - storage.row_insert(vec![watch_val]).await?; - Ok(()) - } -} diff --git a/src/mail/mailbox.rs b/src/mail/mailbox.rs deleted file mode 100644 index d1a5473..0000000 --- a/src/mail/mailbox.rs +++ /dev/null @@ -1,524 +0,0 @@ -use anyhow::{anyhow, bail, Result}; -use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock; - -use crate::bayou::Bayou; -use crate::cryptoblob::{self, gen_key, open_deserialize, seal_serialize, Key}; -use crate::login::Credentials; -use crate::mail::uidindex::*; -use crate::mail::unique_ident::*; -use crate::mail::IMF; -use crate::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store}; -use crate::timestamp::now_msec; - -pub struct Mailbox { - pub(super) id: UniqueIdent, - mbox: RwLock<MailboxInternal>, -} - -impl Mailbox { - pub(crate) async fn open( - creds: &Credentials, - id: UniqueIdent, - min_uidvalidity: ImapUidvalidity, - ) -> Result<Self> { - let index_path = format!("index/{}", id); - let mail_path = format!("mail/{}", id); - - let mut uid_index = Bayou::<UidIndex>::new(creds, index_path).await?; - uid_index.sync().await?; - - let uidvalidity = uid_index.state().uidvalidity; - if uidvalidity < min_uidvalidity { - uid_index - .push( - uid_index - .state() - .op_bump_uidvalidity(min_uidvalidity.get() - uidvalidity.get()), - ) - .await?; - } - - // @FIXME reporting through opentelemetry or some logs - // info on the "shape" of the mailbox would be welcomed - /* - dump(&uid_index); - */ - - let mbox = RwLock::new(MailboxInternal { - id, - encryption_key: creds.keys.master.clone(), - storage: creds.storage.build().await?, - uid_index, - mail_path, - }); - - Ok(Self { id, mbox }) - } - - /// Sync data with backing store - pub async fn force_sync(&self) -> Result<()> { - self.mbox.write().await.force_sync().await - } - - /// Sync data with backing store only if changes are detected - /// or last sync is too old - pub async fn opportunistic_sync(&self) -> Result<()> { - self.mbox.write().await.opportunistic_sync().await - } - - /// Block until a sync has been done (due to changes in the event log) - pub async fn notify(&self) -> std::sync::Weak<tokio::sync::Notify> { - self.mbox.read().await.notifier() - } - - // ---- Functions for reading the mailbox ---- - - /// Get a clone of the current UID Index of this mailbox - /// (cloning is cheap so don't hesitate to use this) - pub async fn current_uid_index(&self) -> UidIndex { - self.mbox.read().await.uid_index.state().clone() - } - - /// Fetch the metadata (headers + some more info) of the specified - /// mail IDs - pub async fn fetch_meta(&self, ids: &[UniqueIdent]) -> Result<Vec<MailMeta>> { - self.mbox.read().await.fetch_meta(ids).await - } - - /// Fetch an entire e-mail - pub async fn fetch_full(&self, id: UniqueIdent, message_key: &Key) -> Result<Vec<u8>> { - self.mbox.read().await.fetch_full(id, message_key).await - } - - pub async fn frozen(self: &std::sync::Arc<Self>) -> super::snapshot::FrozenMailbox { - super::snapshot::FrozenMailbox::new(self.clone()).await - } - - // ---- Functions for changing the mailbox ---- - - /// Add flags to message - pub async fn add_flags<'a>(&self, id: UniqueIdent, flags: &[Flag]) -> Result<()> { - self.mbox.write().await.add_flags(id, flags).await - } - - /// Delete flags from message - pub async fn del_flags<'a>(&self, id: UniqueIdent, flags: &[Flag]) -> Result<()> { - self.mbox.write().await.del_flags(id, flags).await - } - - /// Define the new flags for this message - pub async fn set_flags<'a>(&self, id: UniqueIdent, flags: &[Flag]) -> Result<()> { - self.mbox.write().await.set_flags(id, flags).await - } - - /// Insert an email into the mailbox - pub async fn append<'a>( - &self, - msg: IMF<'a>, - ident: Option<UniqueIdent>, - flags: &[Flag], - ) -> Result<(ImapUidvalidity, ImapUid, ModSeq)> { - self.mbox.write().await.append(msg, ident, flags).await - } - - /// Insert an email into the mailbox, copying it from an existing S3 object - pub async fn append_from_s3<'a>( - &self, - msg: IMF<'a>, - ident: UniqueIdent, - blob_ref: storage::BlobRef, - message_key: Key, - ) -> Result<()> { - self.mbox - .write() - .await - .append_from_s3(msg, ident, blob_ref, message_key) - .await - } - - /// Delete a message definitively from the mailbox - pub async fn delete<'a>(&self, id: UniqueIdent) -> Result<()> { - self.mbox.write().await.delete(id).await - } - - /// Copy an email from an other Mailbox to this mailbox - /// (use this when possible, as it allows for a certain number of storage optimizations) - pub async fn copy_from(&self, from: &Mailbox, uuid: UniqueIdent) -> Result<UniqueIdent> { - if self.id == from.id { - bail!("Cannot copy into same mailbox"); - } - - let (mut selflock, fromlock); - if self.id < from.id { - selflock = self.mbox.write().await; - fromlock = from.mbox.write().await; - } else { - fromlock = from.mbox.write().await; - selflock = self.mbox.write().await; - }; - selflock.copy_from(&fromlock, uuid).await - } - - /// Move an email from an other Mailbox to this mailbox - /// (use this when possible, as it allows for a certain number of storage optimizations) - pub async fn move_from(&self, from: &Mailbox, uuid: UniqueIdent) -> Result<()> { - if self.id == from.id { - bail!("Cannot copy move same mailbox"); - } - - let (mut selflock, mut fromlock); - if self.id < from.id { - selflock = self.mbox.write().await; - fromlock = from.mbox.write().await; - } else { - fromlock = from.mbox.write().await; - selflock = self.mbox.write().await; - }; - selflock.move_from(&mut fromlock, uuid).await - } -} - -// ---- - -// Non standard but common flags: -// https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml -struct MailboxInternal { - // 2023-05-15 will probably be used later. - #[allow(dead_code)] - id: UniqueIdent, - mail_path: String, - encryption_key: Key, - storage: Store, - uid_index: Bayou<UidIndex>, -} - -impl MailboxInternal { - async fn force_sync(&mut self) -> Result<()> { - self.uid_index.sync().await?; - Ok(()) - } - - async fn opportunistic_sync(&mut self) -> Result<()> { - self.uid_index.opportunistic_sync().await?; - Ok(()) - } - - fn notifier(&self) -> std::sync::Weak<tokio::sync::Notify> { - self.uid_index.notifier() - } - - // ---- Functions for reading the mailbox ---- - - async fn fetch_meta(&self, ids: &[UniqueIdent]) -> Result<Vec<MailMeta>> { - let ids = ids.iter().map(|x| x.to_string()).collect::<Vec<_>>(); - let ops = ids - .iter() - .map(|id| RowRef::new(self.mail_path.as_str(), id.as_str())) - .collect::<Vec<_>>(); - let res_vec = self.storage.row_fetch(&Selector::List(ops)).await?; - - let mut meta_vec = vec![]; - for res in res_vec.into_iter() { - let mut meta_opt = None; - - // Resolve conflicts - for v in res.value.iter() { - match v { - storage::Alternative::Tombstone => (), - storage::Alternative::Value(v) => { - let meta = open_deserialize::<MailMeta>(v, &self.encryption_key)?; - match meta_opt.as_mut() { - None => { - meta_opt = Some(meta); - } - Some(prevmeta) => { - prevmeta.try_merge(meta)?; - } - } - } - } - } - if let Some(meta) = meta_opt { - meta_vec.push(meta); - } else { - bail!("No valid meta value in k2v for {:?}", res.row_ref); - } - } - - Ok(meta_vec) - } - - async fn fetch_full(&self, id: UniqueIdent, message_key: &Key) -> Result<Vec<u8>> { - let obj_res = self - .storage - .blob_fetch(&BlobRef(format!("{}/{}", self.mail_path, id))) - .await?; - let body = obj_res.value; - cryptoblob::open(&body, message_key) - } - - // ---- Functions for changing the mailbox ---- - - async fn add_flags(&mut self, ident: UniqueIdent, flags: &[Flag]) -> Result<()> { - let add_flag_op = self.uid_index.state().op_flag_add(ident, flags.to_vec()); - self.uid_index.push(add_flag_op).await - } - - async fn del_flags(&mut self, ident: UniqueIdent, flags: &[Flag]) -> Result<()> { - let del_flag_op = self.uid_index.state().op_flag_del(ident, flags.to_vec()); - self.uid_index.push(del_flag_op).await - } - - async fn set_flags(&mut self, ident: UniqueIdent, flags: &[Flag]) -> Result<()> { - let set_flag_op = self.uid_index.state().op_flag_set(ident, flags.to_vec()); - self.uid_index.push(set_flag_op).await - } - - async fn append( - &mut self, - mail: IMF<'_>, - ident: Option<UniqueIdent>, - flags: &[Flag], - ) -> Result<(ImapUidvalidity, ImapUid, ModSeq)> { - let ident = ident.unwrap_or_else(gen_ident); - let message_key = gen_key(); - - futures::try_join!( - async { - // Encrypt and save mail body - let message_blob = cryptoblob::seal(mail.raw, &message_key)?; - self.storage - .blob_insert(BlobVal::new( - BlobRef(format!("{}/{}", self.mail_path, ident)), - message_blob, - )) - .await?; - Ok::<_, anyhow::Error>(()) - }, - async { - // Save mail meta - let meta = MailMeta { - internaldate: now_msec(), - headers: mail.parsed.raw_headers.to_vec(), - message_key: message_key.clone(), - rfc822_size: mail.raw.len(), - }; - let meta_blob = seal_serialize(&meta, &self.encryption_key)?; - self.storage - .row_insert(vec![RowVal::new( - RowRef::new(&self.mail_path, &ident.to_string()), - meta_blob, - )]) - .await?; - Ok::<_, anyhow::Error>(()) - }, - self.uid_index.opportunistic_sync() - )?; - - // Add mail to Bayou mail index - let uid_state = self.uid_index.state(); - let add_mail_op = uid_state.op_mail_add(ident, flags.to_vec()); - - let uidvalidity = uid_state.uidvalidity; - let (uid, modseq) = match add_mail_op { - UidIndexOp::MailAdd(_, uid, modseq, _) => (uid, modseq), - _ => unreachable!(), - }; - - self.uid_index.push(add_mail_op).await?; - - Ok((uidvalidity, uid, modseq)) - } - - async fn append_from_s3<'a>( - &mut self, - mail: IMF<'a>, - ident: UniqueIdent, - blob_src: storage::BlobRef, - message_key: Key, - ) -> Result<()> { - futures::try_join!( - async { - // Copy mail body from previous location - let blob_dst = BlobRef(format!("{}/{}", self.mail_path, ident)); - self.storage.blob_copy(&blob_src, &blob_dst).await?; - Ok::<_, anyhow::Error>(()) - }, - async { - // Save mail meta - let meta = MailMeta { - internaldate: now_msec(), - headers: mail.parsed.raw_headers.to_vec(), - message_key: message_key.clone(), - rfc822_size: mail.raw.len(), - }; - let meta_blob = seal_serialize(&meta, &self.encryption_key)?; - self.storage - .row_insert(vec![RowVal::new( - RowRef::new(&self.mail_path, &ident.to_string()), - meta_blob, - )]) - .await?; - Ok::<_, anyhow::Error>(()) - }, - self.uid_index.opportunistic_sync() - )?; - - // Add mail to Bayou mail index - let add_mail_op = self.uid_index.state().op_mail_add(ident, vec![]); - self.uid_index.push(add_mail_op).await?; - - Ok(()) - } - - async fn delete(&mut self, ident: UniqueIdent) -> Result<()> { - if !self.uid_index.state().table.contains_key(&ident) { - bail!("Cannot delete mail that doesn't exit"); - } - - let del_mail_op = self.uid_index.state().op_mail_del(ident); - self.uid_index.push(del_mail_op).await?; - - futures::try_join!( - async { - // Delete mail body from S3 - self.storage - .blob_rm(&BlobRef(format!("{}/{}", self.mail_path, ident))) - .await?; - Ok::<_, anyhow::Error>(()) - }, - async { - // Delete mail meta from K2V - let sk = ident.to_string(); - let res = self - .storage - .row_fetch(&storage::Selector::Single(&RowRef::new( - &self.mail_path, - &sk, - ))) - .await?; - if let Some(row_val) = res.into_iter().next() { - self.storage - .row_rm(&storage::Selector::Single(&row_val.row_ref)) - .await?; - } - Ok::<_, anyhow::Error>(()) - } - )?; - Ok(()) - } - - async fn copy_from( - &mut self, - from: &MailboxInternal, - source_id: UniqueIdent, - ) -> Result<UniqueIdent> { - let new_id = gen_ident(); - self.copy_internal(from, source_id, new_id).await?; - Ok(new_id) - } - - async fn move_from(&mut self, from: &mut MailboxInternal, id: UniqueIdent) -> Result<()> { - self.copy_internal(from, id, id).await?; - from.delete(id).await?; - Ok(()) - } - - async fn copy_internal( - &mut self, - from: &MailboxInternal, - source_id: UniqueIdent, - new_id: UniqueIdent, - ) -> Result<()> { - if self.encryption_key != from.encryption_key { - bail!("Message to be copied/moved does not belong to same account."); - } - - let flags = from - .uid_index - .state() - .table - .get(&source_id) - .ok_or(anyhow!("Source mail not found"))? - .2 - .clone(); - - futures::try_join!( - async { - let dst = BlobRef(format!("{}/{}", self.mail_path, new_id)); - let src = BlobRef(format!("{}/{}", from.mail_path, source_id)); - self.storage.blob_copy(&src, &dst).await?; - Ok::<_, anyhow::Error>(()) - }, - async { - // Copy mail meta in K2V - let meta = &from.fetch_meta(&[source_id]).await?[0]; - let meta_blob = seal_serialize(meta, &self.encryption_key)?; - self.storage - .row_insert(vec![RowVal::new( - RowRef::new(&self.mail_path, &new_id.to_string()), - meta_blob, - )]) - .await?; - Ok::<_, anyhow::Error>(()) - }, - self.uid_index.opportunistic_sync(), - )?; - - // Add mail to Bayou mail index - let add_mail_op = self.uid_index.state().op_mail_add(new_id, flags); - self.uid_index.push(add_mail_op).await?; - - Ok(()) - } -} - -// Can be useful to debug so we want this code -// to be available to developers -#[allow(dead_code)] -fn dump(uid_index: &Bayou<UidIndex>) { - let s = uid_index.state(); - println!("---- MAILBOX STATE ----"); - println!("UIDVALIDITY {}", s.uidvalidity); - println!("UIDNEXT {}", s.uidnext); - println!("INTERNALSEQ {}", s.internalseq); - for (uid, ident) in s.idx_by_uid.iter() { - println!( - "{} {} {}", - uid, - hex::encode(ident.0), - s.table.get(ident).cloned().unwrap().2.join(", ") - ); - } - println!(); -} - -// ---- - -/// The metadata of a message that is stored in K2V -/// at pk = mail/<mailbox uuid>, sk = <message uuid> -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MailMeta { - /// INTERNALDATE field (milliseconds since epoch) - pub internaldate: u64, - /// Headers of the message - pub headers: Vec<u8>, - /// Secret key for decrypting entire message - pub message_key: Key, - /// RFC822 size - pub rfc822_size: usize, -} - -impl MailMeta { - fn try_merge(&mut self, other: Self) -> Result<()> { - if self.headers != other.headers - || self.message_key != other.message_key - || self.rfc822_size != other.rfc822_size - { - bail!("Conflicting MailMeta values."); - } - self.internaldate = std::cmp::max(self.internaldate, other.internaldate); - Ok(()) - } -} diff --git a/src/mail/mod.rs b/src/mail/mod.rs deleted file mode 100644 index 03e85cd..0000000 --- a/src/mail/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::convert::TryFrom; - -pub mod incoming; -pub mod mailbox; -pub mod query; -pub mod snapshot; -pub mod uidindex; -pub mod unique_ident; -pub mod namespace; - -// Internet Message Format -// aka RFC 822 - RFC 2822 - RFC 5322 -// 2023-05-15 don't want to refactor this struct now. -#[allow(clippy::upper_case_acronyms)] -pub struct IMF<'a> { - raw: &'a [u8], - parsed: eml_codec::part::composite::Message<'a>, -} - -impl<'a> TryFrom<&'a [u8]> for IMF<'a> { - type Error = (); - - fn try_from(body: &'a [u8]) -> Result<IMF<'a>, ()> { - let parsed = eml_codec::parse_message(body).or(Err(()))?.1; - Ok(Self { raw: body, parsed }) - } -} diff --git a/src/mail/namespace.rs b/src/mail/namespace.rs deleted file mode 100644 index 5e67173..0000000 --- a/src/mail/namespace.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; -use std::sync::{Arc, Weak}; - -use anyhow::{anyhow, bail, Result}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use tokio::sync::watch; - -use crate::cryptoblob::{open_deserialize, seal_serialize}; -use crate::login::Credentials; -use crate::mail::incoming::incoming_mail_watch_process; -use crate::mail::mailbox::Mailbox; -use crate::mail::uidindex::ImapUidvalidity; -use crate::mail::unique_ident::{gen_ident, UniqueIdent}; -use crate::storage; -use crate::timestamp::now_msec; - -pub const MAILBOX_HIERARCHY_DELIMITER: char = '.'; - -/// INBOX is the only mailbox that must always exist. -/// It is created automatically when the account is created. -/// IMAP allows the user to rename INBOX to something else, -/// in this case all messages from INBOX are moved to a mailbox -/// with the new name and the INBOX mailbox still exists and is empty. -/// In our implementation, we indeed move the underlying mailbox -/// to the new name (i.e. the new name has the same id as the previous -/// INBOX), and we create a new empty mailbox for INBOX. -pub const INBOX: &str = "INBOX"; - -/// For convenience purpose, we also create some special mailbox -/// that are described in RFC6154 SPECIAL-USE -/// @FIXME maybe it should be a configuration parameter -/// @FIXME maybe we should have a per-mailbox flag mechanism, either an enum or a string, so we -/// track which mailbox is used for what. -/// @FIXME Junk could be useful but we don't have any antispam solution yet so... -/// @FIXME IMAP supports virtual mailbox. \All or \Flagged are intended to be virtual mailboxes. -/// \Trash might be one, or not one. I don't know what we should do there. -pub const DRAFTS: &str = "Drafts"; -pub const ARCHIVE: &str = "Archive"; -pub const SENT: &str = "Sent"; -pub const TRASH: &str = "Trash"; - -pub(crate) const MAILBOX_LIST_PK: &str = "mailboxes"; -pub(crate) const MAILBOX_LIST_SK: &str = "list"; - -// ---- User's mailbox list (serialized in K2V) ---- - -#[derive(Serialize, Deserialize)] -pub(crate) struct MailboxList(BTreeMap<String, MailboxListEntry>); - -#[derive(Serialize, Deserialize, Clone, Copy, Debug)] -pub(crate) struct MailboxListEntry { - id_lww: (u64, Option<UniqueIdent>), - uidvalidity: ImapUidvalidity, -} - -impl MailboxListEntry { - fn merge(&mut self, other: &Self) { - // Simple CRDT merge rule - if other.id_lww.0 > self.id_lww.0 - || (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1) - { - self.id_lww = other.id_lww; - } - self.uidvalidity = std::cmp::max(self.uidvalidity, other.uidvalidity); - } -} - -impl MailboxList { - pub(crate) fn new() -> Self { - Self(BTreeMap::new()) - } - - pub(crate) fn merge(&mut self, list2: Self) { - for (k, v) in list2.0.into_iter() { - if let Some(e) = self.0.get_mut(&k) { - e.merge(&v); - } else { - self.0.insert(k, v); - } - } - } - - pub(crate) fn existing_mailbox_names(&self) -> Vec<String> { - self.0 - .iter() - .filter(|(_, v)| v.id_lww.1.is_some()) - .map(|(k, _)| k.to_string()) - .collect() - } - - pub(crate) fn has_mailbox(&self, name: &str) -> bool { - matches!( - self.0.get(name), - Some(MailboxListEntry { - id_lww: (_, Some(_)), - .. - }) - ) - } - - pub(crate) fn get_mailbox(&self, name: &str) -> Option<(ImapUidvalidity, Option<UniqueIdent>)> { - self.0.get(name).map( - |MailboxListEntry { - id_lww: (_, mailbox_id), - uidvalidity, - }| (*uidvalidity, *mailbox_id), - ) - } - - /// Ensures mailbox `name` maps to id `id`. - /// If it already mapped to that, returns None. - /// If a change had to be done, returns Some(new uidvalidity in mailbox). - pub(crate) fn set_mailbox(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<ImapUidvalidity> { - let (ts, id, uidvalidity) = match self.0.get_mut(name) { - None => { - if id.is_none() { - return None; - } else { - (now_msec(), id, ImapUidvalidity::new(1).unwrap()) - } - } - Some(MailboxListEntry { - id_lww, - uidvalidity, - }) => { - if id_lww.1 == id { - return None; - } else { - ( - std::cmp::max(id_lww.0 + 1, now_msec()), - id, - ImapUidvalidity::new(uidvalidity.get() + 1).unwrap(), - ) - } - } - }; - - self.0.insert( - name.into(), - MailboxListEntry { - id_lww: (ts, id), - uidvalidity, - }, - ); - Some(uidvalidity) - } - - pub(crate) fn update_uidvalidity(&mut self, name: &str, new_uidvalidity: ImapUidvalidity) { - match self.0.get_mut(name) { - None => { - self.0.insert( - name.into(), - MailboxListEntry { - id_lww: (now_msec(), None), - uidvalidity: new_uidvalidity, - }, - ); - } - Some(MailboxListEntry { uidvalidity, .. }) => { - *uidvalidity = std::cmp::max(*uidvalidity, new_uidvalidity); - } - } - } - - pub(crate) fn create_mailbox(&mut self, name: &str) -> CreatedMailbox { - if let Some(MailboxListEntry { - id_lww: (_, Some(id)), - uidvalidity, - }) = self.0.get(name) - { - return CreatedMailbox::Existed(*id, *uidvalidity); - } - - let id = gen_ident(); - let uidvalidity = self.set_mailbox(name, Some(id)).unwrap(); - CreatedMailbox::Created(id, uidvalidity) - } - - pub(crate) fn rename_mailbox(&mut self, old_name: &str, new_name: &str) -> Result<()> { - if let Some((uidvalidity, Some(mbid))) = self.get_mailbox(old_name) { - if self.has_mailbox(new_name) { - bail!( - "Cannot rename {} into {}: {} already exists", - old_name, - new_name, - new_name - ); - } - - self.set_mailbox(old_name, None); - self.set_mailbox(new_name, Some(mbid)); - self.update_uidvalidity(new_name, uidvalidity); - Ok(()) - } else { - bail!( - "Cannot rename {} into {}: {} doesn't exist", - old_name, - new_name, - old_name - ); - } - } -} - -pub(crate) enum CreatedMailbox { - Created(UniqueIdent, ImapUidvalidity), - Existed(UniqueIdent, ImapUidvalidity), -} diff --git a/src/mail/query.rs b/src/mail/query.rs deleted file mode 100644 index 3e6fe99..0000000 --- a/src/mail/query.rs +++ /dev/null @@ -1,137 +0,0 @@ -use super::mailbox::MailMeta; -use super::snapshot::FrozenMailbox; -use super::unique_ident::UniqueIdent; -use anyhow::Result; -use futures::future::FutureExt; -use futures::stream::{BoxStream, Stream, StreamExt}; - -/// Query is in charge of fetching efficiently -/// requested data for a list of emails -pub struct Query<'a, 'b> { - pub frozen: &'a FrozenMailbox, - pub emails: &'b [UniqueIdent], - pub scope: QueryScope, -} - -#[derive(Debug)] -pub enum QueryScope { - Index, - Partial, - Full, -} -impl QueryScope { - pub fn union(&self, other: &QueryScope) -> QueryScope { - match (self, other) { - (QueryScope::Full, _) | (_, QueryScope::Full) => QueryScope::Full, - (QueryScope::Partial, _) | (_, QueryScope::Partial) => QueryScope::Partial, - (QueryScope::Index, QueryScope::Index) => QueryScope::Index, - } - } -} - -//type QueryResultStream = Box<dyn Stream<Item = Result<QueryResult>>>; - -impl<'a, 'b> Query<'a, 'b> { - pub fn fetch(&self) -> BoxStream<Result<QueryResult>> { - match self.scope { - QueryScope::Index => Box::pin( - futures::stream::iter(self.emails) - .map(|&uuid| Ok(QueryResult::IndexResult { uuid })), - ), - QueryScope::Partial => Box::pin(self.partial()), - QueryScope::Full => Box::pin(self.full()), - } - } - - // --- functions below are private *for reasons* - fn partial<'d>(&'d self) -> impl Stream<Item = Result<QueryResult>> + 'd + Send { - async move { - let maybe_meta_list: Result<Vec<MailMeta>> = - self.frozen.mailbox.fetch_meta(self.emails).await; - let list_res = maybe_meta_list - .map(|meta_list| { - meta_list - .into_iter() - .zip(self.emails) - .map(|(metadata, &uuid)| Ok(QueryResult::PartialResult { uuid, metadata })) - .collect() - }) - .unwrap_or_else(|e| vec![Err(e)]); - - futures::stream::iter(list_res) - } - .flatten_stream() - } - - fn full<'d>(&'d self) -> impl Stream<Item = Result<QueryResult>> + 'd + Send { - self.partial().then(move |maybe_meta| async move { - let meta = maybe_meta?; - - let content = self - .frozen - .mailbox - .fetch_full( - *meta.uuid(), - &meta - .metadata() - .expect("meta to be PartialResult") - .message_key, - ) - .await?; - - Ok(meta.into_full(content).expect("meta to be PartialResult")) - }) - } -} - -#[derive(Debug, Clone)] -pub enum QueryResult { - IndexResult { - uuid: UniqueIdent, - }, - PartialResult { - uuid: UniqueIdent, - metadata: MailMeta, - }, - FullResult { - uuid: UniqueIdent, - metadata: MailMeta, - content: Vec<u8>, - }, -} -impl QueryResult { - pub fn uuid(&self) -> &UniqueIdent { - match self { - Self::IndexResult { uuid, .. } => uuid, - Self::PartialResult { uuid, .. } => uuid, - Self::FullResult { uuid, .. } => uuid, - } - } - - pub fn metadata(&self) -> Option<&MailMeta> { - match self { - Self::IndexResult { .. } => None, - Self::PartialResult { metadata, .. } => Some(metadata), - Self::FullResult { metadata, .. } => Some(metadata), - } - } - - #[allow(dead_code)] - pub fn content(&self) -> Option<&[u8]> { - match self { - Self::FullResult { content, .. } => Some(content), - _ => None, - } - } - - fn into_full(self, content: Vec<u8>) -> Option<Self> { - match self { - Self::PartialResult { uuid, metadata } => Some(Self::FullResult { - uuid, - metadata, - content, - }), - _ => None, - } - } -} diff --git a/src/mail/snapshot.rs b/src/mail/snapshot.rs deleted file mode 100644 index ed756b5..0000000 --- a/src/mail/snapshot.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; - -use super::mailbox::Mailbox; -use super::query::{Query, QueryScope}; -use super::uidindex::UidIndex; -use super::unique_ident::UniqueIdent; - -/// A Frozen Mailbox has a snapshot of the current mailbox -/// state that is desynchronized with the real mailbox state. -/// It's up to the user to choose when their snapshot must be updated -/// to give useful information to their clients -pub struct FrozenMailbox { - pub mailbox: Arc<Mailbox>, - pub snapshot: UidIndex, -} - -impl FrozenMailbox { - /// Create a snapshot from a mailbox, the mailbox + the snapshot - /// becomes the "Frozen Mailbox". - pub async fn new(mailbox: Arc<Mailbox>) -> Self { - let state = mailbox.current_uid_index().await; - - Self { - mailbox, - snapshot: state, - } - } - - /// Force the synchronization of the inner mailbox - /// but do not update the local snapshot - pub async fn sync(&self) -> Result<()> { - self.mailbox.opportunistic_sync().await - } - - /// Peek snapshot without updating the frozen mailbox - /// Can be useful if you want to plan some writes - /// while sending a diff to the client later - pub async fn peek(&self) -> UidIndex { - self.mailbox.current_uid_index().await - } - - /// Update the FrozenMailbox local snapshot. - /// Returns the old snapshot, so you can build a diff - pub async fn update(&mut self) -> UidIndex { - let old_snapshot = self.snapshot.clone(); - self.snapshot = self.mailbox.current_uid_index().await; - - old_snapshot - } - - pub fn query<'a, 'b>(&'a self, uuids: &'b [UniqueIdent], scope: QueryScope) -> Query<'a, 'b> { - Query { - frozen: self, - emails: uuids, - scope, - } - } -} diff --git a/src/mail/uidindex.rs b/src/mail/uidindex.rs deleted file mode 100644 index 5a06670..0000000 --- a/src/mail/uidindex.rs +++ /dev/null @@ -1,474 +0,0 @@ -use std::num::{NonZeroU32, NonZeroU64}; - -use im::{HashMap, OrdMap, OrdSet}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use crate::bayou::*; -use crate::mail::unique_ident::UniqueIdent; - -pub type ModSeq = NonZeroU64; -pub type ImapUid = NonZeroU32; -pub type ImapUidvalidity = NonZeroU32; -pub type Flag = String; -pub type IndexEntry = (ImapUid, ModSeq, Vec<Flag>); - -/// A UidIndex handles the mutable part of a mailbox -/// It is built by running the event log on it -/// Each applied log generates a new UidIndex by cloning the previous one -/// and applying the event. This is why we use immutable datastructures: -/// they are cheap to clone. -#[derive(Clone)] -pub struct UidIndex { - // Source of trust - pub table: OrdMap<UniqueIdent, IndexEntry>, - - // Indexes optimized for queries - pub idx_by_uid: OrdMap<ImapUid, UniqueIdent>, - pub idx_by_modseq: OrdMap<ModSeq, UniqueIdent>, - pub idx_by_flag: FlagIndex, - - // "Public" Counters - pub uidvalidity: ImapUidvalidity, - pub uidnext: ImapUid, - pub highestmodseq: ModSeq, - - // "Internal" Counters - pub internalseq: ImapUid, - pub internalmodseq: ModSeq, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub enum UidIndexOp { - MailAdd(UniqueIdent, ImapUid, ModSeq, Vec<Flag>), - MailDel(UniqueIdent), - FlagAdd(UniqueIdent, ModSeq, Vec<Flag>), - FlagDel(UniqueIdent, ModSeq, Vec<Flag>), - FlagSet(UniqueIdent, ModSeq, Vec<Flag>), - BumpUidvalidity(u32), -} - -impl UidIndex { - #[must_use] - pub fn op_mail_add(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { - UidIndexOp::MailAdd(ident, self.internalseq, self.internalmodseq, flags) - } - - #[must_use] - pub fn op_mail_del(&self, ident: UniqueIdent) -> UidIndexOp { - UidIndexOp::MailDel(ident) - } - - #[must_use] - pub fn op_flag_add(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { - UidIndexOp::FlagAdd(ident, self.internalmodseq, flags) - } - - #[must_use] - pub fn op_flag_del(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { - UidIndexOp::FlagDel(ident, self.internalmodseq, flags) - } - - #[must_use] - pub fn op_flag_set(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { - UidIndexOp::FlagSet(ident, self.internalmodseq, flags) - } - - #[must_use] - pub fn op_bump_uidvalidity(&self, count: u32) -> UidIndexOp { - UidIndexOp::BumpUidvalidity(count) - } - - // INTERNAL functions to keep state consistent - - fn reg_email(&mut self, ident: UniqueIdent, uid: ImapUid, modseq: ModSeq, flags: &[Flag]) { - // Insert the email in our table - self.table.insert(ident, (uid, modseq, flags.to_owned())); - - // Update the indexes/caches - self.idx_by_uid.insert(uid, ident); - self.idx_by_flag.insert(uid, flags); - self.idx_by_modseq.insert(modseq, ident); - } - - fn unreg_email(&mut self, ident: &UniqueIdent) { - // We do nothing if the mail does not exist - let (uid, modseq, flags) = match self.table.get(ident) { - Some(v) => v, - None => return, - }; - - // Delete all cache entries - self.idx_by_uid.remove(uid); - self.idx_by_flag.remove(*uid, flags); - self.idx_by_modseq.remove(modseq); - - // Remove from source of trust - self.table.remove(ident); - } -} - -impl Default for UidIndex { - fn default() -> Self { - Self { - table: OrdMap::new(), - - idx_by_uid: OrdMap::new(), - idx_by_modseq: OrdMap::new(), - idx_by_flag: FlagIndex::new(), - - uidvalidity: NonZeroU32::new(1).unwrap(), - uidnext: NonZeroU32::new(1).unwrap(), - highestmodseq: NonZeroU64::new(1).unwrap(), - - internalseq: NonZeroU32::new(1).unwrap(), - internalmodseq: NonZeroU64::new(1).unwrap(), - } - } -} - -impl BayouState for UidIndex { - type Op = UidIndexOp; - - fn apply(&self, op: &UidIndexOp) -> Self { - let mut new = self.clone(); - match op { - UidIndexOp::MailAdd(ident, uid, modseq, flags) => { - // Change UIDValidity if there is a UID conflict or a MODSEQ conflict - // @FIXME Need to prove that summing work - // The intuition: we increase the UIDValidity by the number of possible conflicts - if *uid < new.internalseq || *modseq < new.internalmodseq { - let bump_uid = new.internalseq.get() - uid.get(); - let bump_modseq = (new.internalmodseq.get() - modseq.get()) as u32; - new.uidvalidity = - NonZeroU32::new(new.uidvalidity.get() + bump_uid + bump_modseq).unwrap(); - } - - // Assign the real uid of the email - let new_uid = new.internalseq; - - // Assign the real modseq of the email and its new flags - let new_modseq = new.internalmodseq; - - // Delete the previous entry if any. - // Our proof has no assumption on `ident` uniqueness, - // so we must handle this case even it is very unlikely - // In this case, we overwrite the email. - // Note: assigning a new UID is mandatory. - new.unreg_email(ident); - - // We record our email and update ou caches - new.reg_email(*ident, new_uid, new_modseq, flags); - - // Update counters - new.highestmodseq = new.internalmodseq; - - new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); - new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); - - new.uidnext = new.internalseq; - } - UidIndexOp::MailDel(ident) => { - // If the email is known locally, we remove its references in all our indexes - new.unreg_email(ident); - - // We update the counter - new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); - } - UidIndexOp::FlagAdd(ident, candidate_modseq, new_flags) => { - if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { - // Bump UIDValidity if required - if *candidate_modseq < new.internalmodseq { - let bump_modseq = - (new.internalmodseq.get() - candidate_modseq.get()) as u32; - new.uidvalidity = - NonZeroU32::new(new.uidvalidity.get() + bump_modseq).unwrap(); - } - - // Add flags to the source of trust and the cache - let mut to_add: Vec<Flag> = new_flags - .iter() - .filter(|f| !existing_flags.contains(f)) - .cloned() - .collect(); - new.idx_by_flag.insert(*uid, &to_add); - *email_modseq = new.internalmodseq; - new.idx_by_modseq.insert(new.internalmodseq, *ident); - existing_flags.append(&mut to_add); - - // Update counters - new.highestmodseq = new.internalmodseq; - new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); - } - } - UidIndexOp::FlagDel(ident, candidate_modseq, rm_flags) => { - if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { - // Bump UIDValidity if required - if *candidate_modseq < new.internalmodseq { - let bump_modseq = - (new.internalmodseq.get() - candidate_modseq.get()) as u32; - new.uidvalidity = - NonZeroU32::new(new.uidvalidity.get() + bump_modseq).unwrap(); - } - - // Remove flags from the source of trust and the cache - existing_flags.retain(|x| !rm_flags.contains(x)); - new.idx_by_flag.remove(*uid, rm_flags); - - // Register that email has been modified - new.idx_by_modseq.insert(new.internalmodseq, *ident); - *email_modseq = new.internalmodseq; - - // Update counters - new.highestmodseq = new.internalmodseq; - new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); - } - } - UidIndexOp::FlagSet(ident, candidate_modseq, new_flags) => { - if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) { - // Bump UIDValidity if required - if *candidate_modseq < new.internalmodseq { - let bump_modseq = - (new.internalmodseq.get() - candidate_modseq.get()) as u32; - new.uidvalidity = - NonZeroU32::new(new.uidvalidity.get() + bump_modseq).unwrap(); - } - - // Remove flags from the source of trust and the cache - let (keep_flags, rm_flags): (Vec<String>, Vec<String>) = existing_flags - .iter() - .cloned() - .partition(|x| new_flags.contains(x)); - *existing_flags = keep_flags; - let mut to_add: Vec<Flag> = new_flags - .iter() - .filter(|f| !existing_flags.contains(f)) - .cloned() - .collect(); - existing_flags.append(&mut to_add); - new.idx_by_flag.remove(*uid, &rm_flags); - new.idx_by_flag.insert(*uid, &to_add); - - // Register that email has been modified - new.idx_by_modseq.insert(new.internalmodseq, *ident); - *email_modseq = new.internalmodseq; - - // Update counters - new.highestmodseq = new.internalmodseq; - new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap(); - } - } - UidIndexOp::BumpUidvalidity(count) => { - new.uidvalidity = ImapUidvalidity::new(new.uidvalidity.get() + *count) - .unwrap_or(ImapUidvalidity::new(u32::MAX).unwrap()); - } - } - new - } -} - -// ---- FlagIndex implementation ---- - -#[derive(Clone)] -pub struct FlagIndex(HashMap<Flag, OrdSet<ImapUid>>); -pub type FlagIter<'a> = im::hashmap::Keys<'a, Flag, OrdSet<ImapUid>>; - -impl FlagIndex { - fn new() -> Self { - Self(HashMap::new()) - } - fn insert(&mut self, uid: ImapUid, flags: &[Flag]) { - flags.iter().for_each(|flag| { - self.0 - .entry(flag.clone()) - .or_insert(OrdSet::new()) - .insert(uid); - }); - } - fn remove(&mut self, uid: ImapUid, flags: &[Flag]) { - for flag in flags.iter() { - if let Some(set) = self.0.get_mut(flag) { - set.remove(&uid); - if set.is_empty() { - self.0.remove(flag); - } - } - } - } - - pub fn get(&self, f: &Flag) -> Option<&OrdSet<ImapUid>> { - self.0.get(f) - } - - pub fn flags(&self) -> FlagIter { - self.0.keys() - } -} - -// ---- CUSTOM SERIALIZATION AND DESERIALIZATION ---- - -#[derive(Serialize, Deserialize)] -struct UidIndexSerializedRepr { - mails: Vec<(ImapUid, ModSeq, UniqueIdent, Vec<Flag>)>, - - uidvalidity: ImapUidvalidity, - uidnext: ImapUid, - highestmodseq: ModSeq, - - internalseq: ImapUid, - internalmodseq: ModSeq, -} - -impl<'de> Deserialize<'de> for UidIndex { - fn deserialize<D>(d: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let val: UidIndexSerializedRepr = UidIndexSerializedRepr::deserialize(d)?; - - let mut uidindex = UidIndex { - table: OrdMap::new(), - - idx_by_uid: OrdMap::new(), - idx_by_modseq: OrdMap::new(), - idx_by_flag: FlagIndex::new(), - - uidvalidity: val.uidvalidity, - uidnext: val.uidnext, - highestmodseq: val.highestmodseq, - - internalseq: val.internalseq, - internalmodseq: val.internalmodseq, - }; - - val.mails - .iter() - .for_each(|(uid, modseq, uuid, flags)| uidindex.reg_email(*uuid, *uid, *modseq, flags)); - - Ok(uidindex) - } -} - -impl Serialize for UidIndex { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - let mut mails = vec![]; - for (ident, (uid, modseq, flags)) in self.table.iter() { - mails.push((*uid, *modseq, *ident, flags.clone())); - } - - let val = UidIndexSerializedRepr { - mails, - uidvalidity: self.uidvalidity, - uidnext: self.uidnext, - highestmodseq: self.highestmodseq, - internalseq: self.internalseq, - internalmodseq: self.internalmodseq, - }; - - val.serialize(serializer) - } -} - -// ---- TESTS ---- - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_uidindex() { - let mut state = UidIndex::default(); - - // Add message 1 - { - let m = UniqueIdent([0x01; 24]); - let f = vec!["\\Recent".to_string(), "\\Archive".to_string()]; - let ev = state.op_mail_add(m, f); - state = state.apply(&ev); - - // Early checks - assert_eq!(state.table.len(), 1); - let (uid, modseq, flags) = state.table.get(&m).unwrap(); - assert_eq!(*uid, NonZeroU32::new(1).unwrap()); - assert_eq!(*modseq, NonZeroU64::new(1).unwrap()); - assert_eq!(flags.len(), 2); - let ident = state.idx_by_uid.get(&NonZeroU32::new(1).unwrap()).unwrap(); - assert_eq!(&m, ident); - let recent = state.idx_by_flag.0.get("\\Recent").unwrap(); - assert_eq!(recent.len(), 1); - assert_eq!(recent.iter().next().unwrap(), &NonZeroU32::new(1).unwrap()); - assert_eq!(state.uidnext, NonZeroU32::new(2).unwrap()); - assert_eq!(state.uidvalidity, NonZeroU32::new(1).unwrap()); - } - - // Add message 2 - { - let m = UniqueIdent([0x02; 24]); - let f = vec!["\\Seen".to_string(), "\\Archive".to_string()]; - let ev = state.op_mail_add(m, f); - state = state.apply(&ev); - - let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); - assert_eq!(archive.len(), 2); - } - - // Add flags to message 1 - { - let m = UniqueIdent([0x01; 24]); - let f = vec!["Important".to_string(), "$cl_1".to_string()]; - let ev = state.op_flag_add(m, f); - state = state.apply(&ev); - } - - // Delete flags from message 1 - { - let m = UniqueIdent([0x01; 24]); - let f = vec!["\\Recent".to_string()]; - let ev = state.op_flag_del(m, f); - state = state.apply(&ev); - - let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); - assert_eq!(archive.len(), 2); - } - - // Delete message 2 - { - let m = UniqueIdent([0x02; 24]); - let ev = state.op_mail_del(m); - state = state.apply(&ev); - - let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); - assert_eq!(archive.len(), 1); - } - - // Add a message 3 concurrent to message 1 (trigger a uid validity change) - { - let m = UniqueIdent([0x03; 24]); - let f = vec!["\\Archive".to_string(), "\\Recent".to_string()]; - let ev = UidIndexOp::MailAdd( - m, - NonZeroU32::new(1).unwrap(), - NonZeroU64::new(1).unwrap(), - f, - ); - state = state.apply(&ev); - } - - // Checks - { - assert_eq!(state.table.len(), 2); - assert!(state.uidvalidity > NonZeroU32::new(1).unwrap()); - - let (last_uid, ident) = state.idx_by_uid.get_max().unwrap(); - assert_eq!(ident, &UniqueIdent([0x03; 24])); - - let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); - assert_eq!(archive.len(), 2); - let mut iter = archive.iter(); - assert_eq!(iter.next().unwrap(), &NonZeroU32::new(1).unwrap()); - assert_eq!(iter.next().unwrap(), last_uid); - } - } -} diff --git a/src/mail/unique_ident.rs b/src/mail/unique_ident.rs deleted file mode 100644 index 0e629db..0000000 --- a/src/mail/unique_ident.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::str::FromStr; -use std::sync::atomic::{AtomicU64, Ordering}; - -use lazy_static::lazy_static; -use rand::prelude::*; -use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; - -use crate::timestamp::now_msec; - -/// An internal Mail Identifier is composed of two components: -/// - a process identifier, 128 bits, itself composed of: -/// - the timestamp of when the process started, 64 bits -/// - a 64-bit random number -/// - a sequence number, 64 bits -/// They are not part of the protocol but an internal representation -/// required by Aerogramme. -/// Their main property is to be unique without having to rely -/// on synchronization between IMAP processes. -#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub struct UniqueIdent(pub [u8; 24]); - -struct IdentGenerator { - pid: u128, - sn: AtomicU64, -} - -impl IdentGenerator { - fn new() -> Self { - let time = now_msec() as u128; - let rand = thread_rng().gen::<u64>() as u128; - Self { - pid: (time << 64) | rand, - sn: AtomicU64::new(0), - } - } - - fn gen(&self) -> UniqueIdent { - let sn = self.sn.fetch_add(1, Ordering::Relaxed); - let mut res = [0u8; 24]; - res[0..16].copy_from_slice(&u128::to_be_bytes(self.pid)); - res[16..24].copy_from_slice(&u64::to_be_bytes(sn)); - UniqueIdent(res) - } -} - -lazy_static! { - static ref GENERATOR: IdentGenerator = IdentGenerator::new(); -} - -pub fn gen_ident() -> UniqueIdent { - GENERATOR.gen() -} - -// -- serde -- - -impl<'de> Deserialize<'de> for UniqueIdent { - fn deserialize<D>(d: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let v = String::deserialize(d)?; - UniqueIdent::from_str(&v).map_err(D::Error::custom) - } -} - -impl Serialize for UniqueIdent { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::fmt::Display for UniqueIdent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) - } -} - -impl std::fmt::Debug for UniqueIdent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.0)) - } -} - -impl FromStr for UniqueIdent { - type Err = &'static str; - - fn from_str(s: &str) -> Result<UniqueIdent, &'static str> { - let bytes = hex::decode(s).map_err(|_| "invalid hex")?; - - if bytes.len() != 24 { - return Err("bad length"); - } - - let mut tmp = [0u8; 24]; - tmp[..].copy_from_slice(&bytes); - Ok(UniqueIdent(tmp)) - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 43b4dca..0000000 --- a/src/main.rs +++ /dev/null @@ -1,407 +0,0 @@ -use std::io::Read; -use std::path::PathBuf; - -use anyhow::{bail, Context, Result}; -use clap::{Parser, Subcommand}; -use nix::{sys::signal, unistd::Pid}; - -use aerogramme::config::*; -use aerogramme::login::{static_provider::*, *}; -use aerogramme::server::Server; - -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Args { - #[clap(subcommand)] - command: Command, - - /// A special mode dedicated to developers, NOT INTENDED FOR PRODUCTION - #[clap(long)] - dev: bool, - - #[clap( - short, - long, - env = "AEROGRAMME_CONFIG", - default_value = "aerogramme.toml" - )] - /// Path to the main Aerogramme configuration file - config_file: PathBuf, -} - -#[derive(Subcommand, Debug)] -enum Command { - #[clap(subcommand)] - /// A daemon to be run by the end user, on a personal device - Companion(CompanionCommand), - - #[clap(subcommand)] - /// A daemon to be run by the service provider, on a server - Provider(ProviderCommand), - - #[clap(subcommand)] - /// Specific tooling, should not be part of a normal workflow, for debug & experimentation only - Tools(ToolsCommand), - //Test, -} - -#[derive(Subcommand, Debug)] -enum ToolsCommand { - /// Manage crypto roots - #[clap(subcommand)] - CryptoRoot(CryptoRootCommand), - - PasswordHash { - #[clap(env = "AEROGRAMME_PASSWORD")] - maybe_password: Option<String>, - }, -} - -#[derive(Subcommand, Debug)] -enum CryptoRootCommand { - /// Generate a new crypto-root protected with a password - New { - #[clap(env = "AEROGRAMME_PASSWORD")] - maybe_password: Option<String>, - }, - /// Generate a new clear text crypto-root, store it securely! - NewClearText, - /// Change the password of a crypto key - ChangePassword { - #[clap(env = "AEROGRAMME_OLD_PASSWORD")] - maybe_old_password: Option<String>, - - #[clap(env = "AEROGRAMME_NEW_PASSWORD")] - maybe_new_password: Option<String>, - - #[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")] - crypto_root: String, - }, - /// From a given crypto-key, derive one containing only the public key - DeriveIncoming { - #[clap(short, long, env = "AEROGRAMME_CRYPTO_ROOT")] - crypto_root: String, - }, -} - -#[derive(Subcommand, Debug)] -enum CompanionCommand { - /// Runs the IMAP proxy - Daemon, - Reload { - #[clap(short, long, env = "AEROGRAMME_PID")] - pid: Option<i32>, - }, - Wizard, - #[clap(subcommand)] - Account(AccountManagement), -} - -#[derive(Subcommand, Debug)] -enum ProviderCommand { - /// Runs the IMAP+LMTP server daemon - Daemon, - /// Reload the daemon - Reload { - #[clap(short, long, env = "AEROGRAMME_PID")] - pid: Option<i32>, - }, - /// Manage static accounts - #[clap(subcommand)] - Account(AccountManagement), -} - -#[derive(Subcommand, Debug)] -enum AccountManagement { - /// Add an account - Add { - #[clap(short, long)] - login: String, - #[clap(short, long)] - setup: PathBuf, - }, - /// Delete an account - Delete { - #[clap(short, long)] - login: String, - }, - /// Change password for a given account - ChangePassword { - #[clap(env = "AEROGRAMME_OLD_PASSWORD")] - maybe_old_password: Option<String>, - - #[clap(env = "AEROGRAMME_NEW_PASSWORD")] - maybe_new_password: Option<String>, - - #[clap(short, long)] - login: String, - }, -} - -#[cfg(tokio_unstable)] -fn tracer() { - console_subscriber::init(); -} - -#[cfg(not(tokio_unstable))] -fn tracer() { - tracing_subscriber::fmt::init(); -} - -#[tokio::main] -async fn main() -> Result<()> { - if std::env::var("RUST_LOG").is_err() { - std::env::set_var("RUST_LOG", "main=info,aerogramme=info,k2v_client=info") - } - - // Abort on panic (same behavior as in Go) - std::panic::set_hook(Box::new(|panic_info| { - eprintln!("{}", panic_info); - eprintln!("{:?}", backtrace::Backtrace::new()); - std::process::abort(); - })); - - tracer(); - - let args = Args::parse(); - let any_config = if args.dev { - use std::net::*; - AnyConfig::Provider(ProviderConfig { - pid: None, - imap: None, - imap_unsecure: Some(ImapUnsecureConfig { - bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1143), - }), - dav_unsecure: Some(DavUnsecureConfig { - bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8087), - }), - lmtp: Some(LmtpConfig { - bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 1025), - hostname: "example.tld".to_string(), - }), - auth: Some(AuthConfig { - bind_addr: SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), - 12345, - ), - }), - users: UserManagement::Demo, - }) - } else { - read_config(args.config_file)? - }; - - match (&args.command, any_config) { - (Command::Companion(subcommand), AnyConfig::Companion(config)) => match subcommand { - CompanionCommand::Daemon => { - let server = Server::from_companion_config(config).await?; - server.run().await?; - } - CompanionCommand::Reload { pid } => reload(*pid, config.pid)?, - CompanionCommand::Wizard => { - unimplemented!(); - } - CompanionCommand::Account(cmd) => { - let user_file = config.users.user_list; - account_management(&args.command, cmd, user_file)?; - } - }, - (Command::Provider(subcommand), AnyConfig::Provider(config)) => match subcommand { - ProviderCommand::Daemon => { - let server = Server::from_provider_config(config).await?; - server.run().await?; - } - ProviderCommand::Reload { pid } => reload(*pid, config.pid)?, - ProviderCommand::Account(cmd) => { - let user_file = match config.users { - UserManagement::Static(conf) => conf.user_list, - _ => { - panic!("Only static account management is supported from Aerogramme.") - } - }; - account_management(&args.command, cmd, user_file)?; - } - }, - (Command::Provider(_), AnyConfig::Companion(_)) => { - bail!("Your want to run a 'Provider' command but your configuration file has role 'Companion'."); - } - (Command::Companion(_), AnyConfig::Provider(_)) => { - bail!("Your want to run a 'Companion' command but your configuration file has role 'Provider'."); - } - (Command::Tools(subcommand), _) => match subcommand { - ToolsCommand::PasswordHash { maybe_password } => { - let password = match maybe_password { - Some(pwd) => pwd.clone(), - None => rpassword::prompt_password("Enter password: ")?, - }; - println!("{}", hash_password(&password)?); - } - ToolsCommand::CryptoRoot(crcommand) => match crcommand { - CryptoRootCommand::New { maybe_password } => { - let password = match maybe_password { - Some(pwd) => pwd.clone(), - None => { - let password = rpassword::prompt_password("Enter password: ")?; - let password_confirm = - rpassword::prompt_password("Confirm password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - password - } - }; - let crypto_keys = CryptoKeys::init(); - let cr = CryptoRoot::create_pass(&password, &crypto_keys)?; - println!("{}", cr.0); - } - CryptoRootCommand::NewClearText => { - let crypto_keys = CryptoKeys::init(); - let cr = CryptoRoot::create_cleartext(&crypto_keys); - println!("{}", cr.0); - } - CryptoRootCommand::ChangePassword { - maybe_old_password, - maybe_new_password, - crypto_root, - } => { - let old_password = match maybe_old_password { - Some(pwd) => pwd.to_string(), - None => rpassword::prompt_password("Enter old password: ")?, - }; - - let new_password = match maybe_new_password { - Some(pwd) => pwd.to_string(), - None => { - let password = rpassword::prompt_password("Enter new password: ")?; - let password_confirm = - rpassword::prompt_password("Confirm new password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - password - } - }; - - let keys = CryptoRoot(crypto_root.to_string()).crypto_keys(&old_password)?; - let cr = CryptoRoot::create_pass(&new_password, &keys)?; - println!("{}", cr.0); - } - CryptoRootCommand::DeriveIncoming { crypto_root } => { - let pubkey = CryptoRoot(crypto_root.to_string()).public_key()?; - let cr = CryptoRoot::create_incoming(&pubkey); - println!("{}", cr.0); - } - }, - }, - } - - Ok(()) -} - -fn reload(pid: Option<i32>, pid_path: Option<PathBuf>) -> Result<()> { - let final_pid = match (pid, pid_path) { - (Some(pid), _) => pid, - (_, Some(path)) => { - let mut f = std::fs::OpenOptions::new().read(true).open(path)?; - let mut pidstr = String::new(); - f.read_to_string(&mut pidstr)?; - pidstr.parse::<i32>()? - } - _ => bail!("Unable to infer your daemon's PID"), - }; - let pid = Pid::from_raw(final_pid); - signal::kill(pid, signal::Signal::SIGUSR1)?; - Ok(()) -} - -fn account_management(root: &Command, cmd: &AccountManagement, users: PathBuf) -> Result<()> { - let mut ulist: UserList = - read_config(users.clone()).context(format!("'{:?}' must be a user database", users))?; - - match cmd { - AccountManagement::Add { login, setup } => { - tracing::debug!(user = login, "will-create"); - let stp: SetupEntry = read_config(setup.clone()) - .context(format!("'{:?}' must be a setup file", setup))?; - tracing::debug!(user = login, "loaded setup entry"); - - let password = match stp.clear_password { - Some(pwd) => pwd, - None => { - let password = rpassword::prompt_password("Enter password: ")?; - let password_confirm = rpassword::prompt_password("Confirm password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - password - } - }; - - let crypto_keys = CryptoKeys::init(); - let crypto_root = match root { - Command::Provider(_) => CryptoRoot::create_pass(&password, &crypto_keys)?, - Command::Companion(_) => CryptoRoot::create_cleartext(&crypto_keys), - _ => unreachable!(), - }; - - let hash = hash_password(password.as_str()).context("unable to hash password")?; - - ulist.insert( - login.clone(), - UserEntry { - email_addresses: stp.email_addresses, - password: hash, - crypto_root: crypto_root.0, - storage: stp.storage, - }, - ); - - write_config(users.clone(), &ulist)?; - } - AccountManagement::Delete { login } => { - tracing::debug!(user = login, "will-delete"); - ulist.remove(login); - write_config(users.clone(), &ulist)?; - } - AccountManagement::ChangePassword { - maybe_old_password, - maybe_new_password, - login, - } => { - let mut user = ulist.remove(login).context("user must exist first")?; - - let old_password = match maybe_old_password { - Some(pwd) => pwd.to_string(), - None => rpassword::prompt_password("Enter old password: ")?, - }; - - if !verify_password(&old_password, &user.password)? { - bail!(format!("invalid password for login {}", login)); - } - - let crypto_keys = CryptoRoot(user.crypto_root).crypto_keys(&old_password)?; - - let new_password = match maybe_new_password { - Some(pwd) => pwd.to_string(), - None => { - let password = rpassword::prompt_password("Enter new password: ")?; - let password_confirm = rpassword::prompt_password("Confirm new password: ")?; - if password != password_confirm { - bail!("Passwords don't match."); - } - password - } - }; - let new_hash = hash_password(&new_password)?; - let new_crypto_root = CryptoRoot::create_pass(&new_password, &crypto_keys)?; - - user.password = new_hash; - user.crypto_root = new_crypto_root.0; - - ulist.insert(login.clone(), user); - write_config(users.clone(), &ulist)?; - } - }; - - Ok(()) -} diff --git a/src/server.rs b/src/server.rs deleted file mode 100644 index 09e91ad..0000000 --- a/src/server.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::io::Write; -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use futures::try_join; -use log::*; -use tokio::sync::watch; - -use crate::auth; -use crate::config::*; -use crate::dav; -use crate::imap; -use crate::lmtp::*; -use crate::login::ArcLoginProvider; -use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*}; - -pub struct Server { - lmtp_server: Option<Arc<LmtpServer>>, - imap_unsecure_server: Option<imap::Server>, - imap_server: Option<imap::Server>, - auth_server: Option<auth::AuthServer>, - dav_unsecure_server: Option<dav::Server>, - pid_file: Option<PathBuf>, -} - -impl Server { - pub async fn from_companion_config(config: CompanionConfig) -> Result<Self> { - tracing::info!("Init as companion"); - let login = Arc::new(StaticLoginProvider::new(config.users).await?); - - let lmtp_server = None; - let imap_unsecure_server = Some(imap::new_unsecure(config.imap, login.clone())); - Ok(Self { - lmtp_server, - imap_unsecure_server, - imap_server: None, - auth_server: None, - dav_unsecure_server: None, - pid_file: config.pid, - }) - } - - pub async fn from_provider_config(config: ProviderConfig) -> Result<Self> { - tracing::info!("Init as provider"); - let login: ArcLoginProvider = match config.users { - UserManagement::Demo => Arc::new(DemoLoginProvider::new()), - UserManagement::Static(x) => Arc::new(StaticLoginProvider::new(x).await?), - UserManagement::Ldap(x) => Arc::new(LdapLoginProvider::new(x)?), - }; - - let lmtp_server = config.lmtp.map(|lmtp| LmtpServer::new(lmtp, login.clone())); - let imap_unsecure_server = config - .imap_unsecure - .map(|imap| imap::new_unsecure(imap, login.clone())); - let imap_server = config - .imap - .map(|imap| imap::new(imap, login.clone())) - .transpose()?; - let auth_server = config - .auth - .map(|auth| auth::AuthServer::new(auth, login.clone())); - let dav_unsecure_server = config - .dav_unsecure - .map(|dav_config| dav::new_unsecure(dav_config, login.clone())); - - Ok(Self { - lmtp_server, - imap_unsecure_server, - imap_server, - dav_unsecure_server, - auth_server, - pid_file: config.pid, - }) - } - - pub async fn run(self) -> Result<()> { - let pid = std::process::id(); - tracing::info!(pid = pid, "Starting main loops"); - - // write the pid file - if let Some(pid_file) = self.pid_file { - let mut file = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(pid_file)?; - file.write_all(pid.to_string().as_bytes())?; - drop(file); - } - - let (exit_signal, provoke_exit) = watch_ctrl_c(); - let _exit_on_err = move |err: anyhow::Error| { - error!("Error: {}", err); - let _ = provoke_exit.send(true); - }; - - try_join!( - async { - match self.lmtp_server.as_ref() { - None => Ok(()), - Some(s) => s.run(exit_signal.clone()).await, - } - }, - async { - match self.imap_unsecure_server { - None => Ok(()), - Some(s) => s.run(exit_signal.clone()).await, - } - }, - async { - match self.imap_server { - None => Ok(()), - Some(s) => s.run(exit_signal.clone()).await, - } - }, - async { - match self.auth_server { - None => Ok(()), - Some(a) => a.run(exit_signal.clone()).await, - } - }, - async { - match self.dav_unsecure_server { - None => Ok(()), - Some(s) => s.run(exit_signal.clone()).await, - } - } - )?; - - Ok(()) - } -} - -pub fn watch_ctrl_c() -> (watch::Receiver<bool>, Arc<watch::Sender<bool>>) { - let (send_cancel, watch_cancel) = watch::channel(false); - let send_cancel = Arc::new(send_cancel); - let send_cancel_2 = send_cancel.clone(); - tokio::spawn(async move { - tokio::signal::ctrl_c() - .await - .expect("failed to install CTRL+C signal handler"); - info!("Received CTRL+C, shutting down."); - send_cancel.send(true).unwrap(); - }); - (watch_cancel, send_cancel_2) -} diff --git a/src/storage/garage.rs b/src/storage/garage.rs deleted file mode 100644 index 7152764..0000000 --- a/src/storage/garage.rs +++ /dev/null @@ -1,538 +0,0 @@ -use aws_sdk_s3::{self as s3, error::SdkError, operation::get_object::GetObjectError}; -use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; -use aws_smithy_runtime_api::client::http::SharedHttpClient; -use hyper_rustls::HttpsConnector; -use hyper_util::client::legacy::{connect::HttpConnector, Client as HttpClient}; -use hyper_util::rt::TokioExecutor; -use serde::Serialize; - -use crate::storage::*; - -pub struct GarageRoot { - k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>, - aws_http: SharedHttpClient, -} - -impl GarageRoot { - pub fn new() -> anyhow::Result<Self> { - let connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_native_roots()? - .https_or_http() - .enable_http1() - .enable_http2() - .build(); - let k2v_http = HttpClient::builder(TokioExecutor::new()).build(connector); - let aws_http = HyperClientBuilder::new().build_https(); - Ok(Self { k2v_http, aws_http }) - } - - pub fn user(&self, conf: GarageConf) -> anyhow::Result<Arc<GarageUser>> { - let mut unicity: Vec<u8> = vec![]; - unicity.extend_from_slice(file!().as_bytes()); - unicity.append(&mut rmp_serde::to_vec(&conf)?); - - Ok(Arc::new(GarageUser { - conf, - aws_http: self.aws_http.clone(), - k2v_http: self.k2v_http.clone(), - unicity, - })) - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct GarageConf { - pub region: String, - pub s3_endpoint: String, - pub k2v_endpoint: String, - pub aws_access_key_id: String, - pub aws_secret_access_key: String, - pub bucket: String, -} - -//@FIXME we should get rid of this builder -//and allocate a S3 + K2V client only once per user -//(and using a shared HTTP client) -#[derive(Clone, Debug)] -pub struct GarageUser { - conf: GarageConf, - aws_http: SharedHttpClient, - k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>, - unicity: Vec<u8>, -} - -#[async_trait] -impl IBuilder for GarageUser { - async fn build(&self) -> Result<Store, StorageError> { - let s3_creds = s3::config::Credentials::new( - self.conf.aws_access_key_id.clone(), - self.conf.aws_secret_access_key.clone(), - None, - None, - "aerogramme", - ); - - let sdk_config = aws_config::from_env() - .region(aws_config::Region::new(self.conf.region.clone())) - .credentials_provider(s3_creds) - .http_client(self.aws_http.clone()) - .endpoint_url(self.conf.s3_endpoint.clone()) - .load() - .await; - - let s3_config = aws_sdk_s3::config::Builder::from(&sdk_config) - .force_path_style(true) - .build(); - - let s3_client = aws_sdk_s3::Client::from_conf(s3_config); - - let k2v_config = k2v_client::K2vClientConfig { - endpoint: self.conf.k2v_endpoint.clone(), - region: self.conf.region.clone(), - aws_access_key_id: self.conf.aws_access_key_id.clone(), - aws_secret_access_key: self.conf.aws_secret_access_key.clone(), - bucket: self.conf.bucket.clone(), - user_agent: None, - }; - - let k2v_client = - match k2v_client::K2vClient::new_with_client(k2v_config, self.k2v_http.clone()) { - Err(e) => { - tracing::error!("unable to build k2v client: {}", e); - return Err(StorageError::Internal); - } - Ok(v) => v, - }; - - Ok(Box::new(GarageStore { - bucket: self.conf.bucket.clone(), - s3: s3_client, - k2v: k2v_client, - })) - } - fn unique(&self) -> UnicityBuffer { - UnicityBuffer(self.unicity.clone()) - } -} - -pub struct GarageStore { - bucket: String, - s3: s3::Client, - k2v: k2v_client::K2vClient, -} - -fn causal_to_row_val(row_ref: RowRef, causal_value: k2v_client::CausalValue) -> RowVal { - let new_row_ref = row_ref.with_causality(causal_value.causality.into()); - let row_values = causal_value - .value - .into_iter() - .map(|k2v_value| match k2v_value { - k2v_client::K2vValue::Tombstone => Alternative::Tombstone, - k2v_client::K2vValue::Value(v) => Alternative::Value(v), - }) - .collect::<Vec<_>>(); - - RowVal { - row_ref: new_row_ref, - value: row_values, - } -} - -#[async_trait] -impl IStore for GarageStore { - async fn row_fetch<'a>(&self, select: &Selector<'a>) -> Result<Vec<RowVal>, StorageError> { - tracing::trace!(select=%select, command="row_fetch"); - let (pk_list, batch_op) = match select { - Selector::Range { - shard, - sort_begin, - sort_end, - } => ( - vec![shard.to_string()], - vec![k2v_client::BatchReadOp { - partition_key: shard, - filter: k2v_client::Filter { - start: Some(sort_begin), - end: Some(sort_end), - ..k2v_client::Filter::default() - }, - ..k2v_client::BatchReadOp::default() - }], - ), - Selector::List(row_ref_list) => ( - row_ref_list - .iter() - .map(|row_ref| row_ref.uid.shard.to_string()) - .collect::<Vec<_>>(), - row_ref_list - .iter() - .map(|row_ref| k2v_client::BatchReadOp { - partition_key: &row_ref.uid.shard, - filter: k2v_client::Filter { - start: Some(&row_ref.uid.sort), - ..k2v_client::Filter::default() - }, - single_item: true, - ..k2v_client::BatchReadOp::default() - }) - .collect::<Vec<_>>(), - ), - Selector::Prefix { shard, sort_prefix } => ( - vec![shard.to_string()], - vec![k2v_client::BatchReadOp { - partition_key: shard, - filter: k2v_client::Filter { - prefix: Some(sort_prefix), - ..k2v_client::Filter::default() - }, - ..k2v_client::BatchReadOp::default() - }], - ), - Selector::Single(row_ref) => { - let causal_value = match self - .k2v - .read_item(&row_ref.uid.shard, &row_ref.uid.sort) - .await - { - Err(k2v_client::Error::NotFound) => { - tracing::debug!( - "K2V item not found shard={}, sort={}, bucket={}", - row_ref.uid.shard, - row_ref.uid.sort, - self.bucket, - ); - return Err(StorageError::NotFound); - } - Err(e) => { - tracing::error!( - "K2V read item shard={}, sort={}, bucket={} failed: {}", - row_ref.uid.shard, - row_ref.uid.sort, - self.bucket, - e - ); - return Err(StorageError::Internal); - } - Ok(v) => v, - }; - - let row_val = causal_to_row_val((*row_ref).clone(), causal_value); - return Ok(vec![row_val]); - } - }; - - let all_raw_res = match self.k2v.read_batch(&batch_op).await { - Err(e) => { - tracing::error!( - "k2v read batch failed for {:?}, bucket {} with err: {}", - select, - self.bucket, - e - ); - return Err(StorageError::Internal); - } - Ok(v) => v, - }; - //println!("fetch res -> {:?}", all_raw_res); - - let row_vals = - all_raw_res - .into_iter() - .zip(pk_list.into_iter()) - .fold(vec![], |mut acc, (page, pk)| { - page.items - .into_iter() - .map(|(sk, cv)| causal_to_row_val(RowRef::new(&pk, &sk), cv)) - .for_each(|rr| acc.push(rr)); - - acc - }); - tracing::debug!(fetch_count = row_vals.len(), command = "row_fetch"); - - Ok(row_vals) - } - async fn row_rm<'a>(&self, select: &Selector<'a>) -> Result<(), StorageError> { - tracing::trace!(select=%select, command="row_rm"); - let del_op = match select { - Selector::Range { - shard, - sort_begin, - sort_end, - } => vec![k2v_client::BatchDeleteOp { - partition_key: shard, - prefix: None, - start: Some(sort_begin), - end: Some(sort_end), - single_item: false, - }], - Selector::List(row_ref_list) => { - // Insert null values with causality token = delete - let batch_op = row_ref_list - .iter() - .map(|v| k2v_client::BatchInsertOp { - partition_key: &v.uid.shard, - sort_key: &v.uid.sort, - causality: v.causality.clone().map(|ct| ct.into()), - value: k2v_client::K2vValue::Tombstone, - }) - .collect::<Vec<_>>(); - - return match self.k2v.insert_batch(&batch_op).await { - Err(e) => { - tracing::error!("Unable to delete the list of values: {}", e); - Err(StorageError::Internal) - } - Ok(_) => Ok(()), - }; - } - Selector::Prefix { shard, sort_prefix } => vec![k2v_client::BatchDeleteOp { - partition_key: shard, - prefix: Some(sort_prefix), - start: None, - end: None, - single_item: false, - }], - Selector::Single(row_ref) => { - // Insert null values with causality token = delete - let batch_op = vec![k2v_client::BatchInsertOp { - partition_key: &row_ref.uid.shard, - sort_key: &row_ref.uid.sort, - causality: row_ref.causality.clone().map(|ct| ct.into()), - value: k2v_client::K2vValue::Tombstone, - }]; - - return match self.k2v.insert_batch(&batch_op).await { - Err(e) => { - tracing::error!("Unable to delete the list of values: {}", e); - Err(StorageError::Internal) - } - Ok(_) => Ok(()), - }; - } - }; - - // Finally here we only have prefix & range - match self.k2v.delete_batch(&del_op).await { - Err(e) => { - tracing::error!("delete batch error: {}", e); - Err(StorageError::Internal) - } - Ok(_) => Ok(()), - } - } - - async fn row_insert(&self, values: Vec<RowVal>) -> Result<(), StorageError> { - tracing::trace!(entries=%values.iter().map(|v| v.row_ref.to_string()).collect::<Vec<_>>().join(","), command="row_insert"); - let batch_ops = values - .iter() - .map(|v| k2v_client::BatchInsertOp { - partition_key: &v.row_ref.uid.shard, - sort_key: &v.row_ref.uid.sort, - causality: v.row_ref.causality.clone().map(|ct| ct.into()), - value: v - .value - .iter() - .next() - .map(|cv| match cv { - Alternative::Value(buff) => k2v_client::K2vValue::Value(buff.clone()), - Alternative::Tombstone => k2v_client::K2vValue::Tombstone, - }) - .unwrap_or(k2v_client::K2vValue::Tombstone), - }) - .collect::<Vec<_>>(); - - match self.k2v.insert_batch(&batch_ops).await { - Err(e) => { - tracing::error!("k2v can't insert some value: {}", e); - Err(StorageError::Internal) - } - Ok(v) => Ok(v), - } - } - async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError> { - tracing::trace!(entry=%value, command="row_poll"); - loop { - if let Some(ct) = &value.causality { - match self - .k2v - .poll_item(&value.uid.shard, &value.uid.sort, ct.clone().into(), None) - .await - { - Err(e) => { - tracing::error!("Unable to poll item: {}", e); - return Err(StorageError::Internal); - } - Ok(None) => continue, - Ok(Some(cv)) => return Ok(causal_to_row_val(value.clone(), cv)), - } - } else { - match self.k2v.read_item(&value.uid.shard, &value.uid.sort).await { - Err(k2v_client::Error::NotFound) => { - self.k2v - .insert_item(&value.uid.shard, &value.uid.sort, vec![0u8], None) - .await - .map_err(|e| { - tracing::error!("Unable to insert item in polling logic: {}", e); - StorageError::Internal - })?; - } - Err(e) => { - tracing::error!("Unable to read item in polling logic: {}", e); - return Err(StorageError::Internal); - } - Ok(cv) => return Ok(causal_to_row_val(value.clone(), cv)), - } - } - } - } - - async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError> { - tracing::trace!(entry=%blob_ref, command="blob_fetch"); - let maybe_out = self - .s3 - .get_object() - .bucket(self.bucket.to_string()) - .key(blob_ref.0.to_string()) - .send() - .await; - - let object_output = match maybe_out { - Ok(output) => output, - Err(SdkError::ServiceError(x)) => match x.err() { - GetObjectError::NoSuchKey(_) => return Err(StorageError::NotFound), - e => { - tracing::warn!("Blob Fetch Error, Service Error: {}", e); - return Err(StorageError::Internal); - } - }, - Err(e) => { - tracing::warn!("Blob Fetch Error, {}", e); - return Err(StorageError::Internal); - } - }; - - let buffer = match object_output.body.collect().await { - Ok(aggreg) => aggreg.to_vec(), - Err(e) => { - tracing::warn!("Fetching body failed with {}", e); - return Err(StorageError::Internal); - } - }; - - let mut bv = BlobVal::new(blob_ref.clone(), buffer); - if let Some(meta) = object_output.metadata { - bv.meta = meta; - } - tracing::debug!("Fetched {}/{}", self.bucket, blob_ref.0); - Ok(bv) - } - async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> { - tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert"); - let streamable_value = s3::primitives::ByteStream::from(blob_val.value); - - let maybe_send = self - .s3 - .put_object() - .bucket(self.bucket.to_string()) - .key(blob_val.blob_ref.0.to_string()) - .set_metadata(Some(blob_val.meta)) - .body(streamable_value) - .send() - .await; - - match maybe_send { - Err(e) => { - tracing::error!("unable to send object: {}", e); - Err(StorageError::Internal) - } - Ok(_) => { - tracing::debug!("Inserted {}/{}", self.bucket, blob_val.blob_ref.0); - Ok(()) - } - } - } - async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> { - tracing::trace!(src=%src, dst=%dst, command="blob_copy"); - let maybe_copy = self - .s3 - .copy_object() - .bucket(self.bucket.to_string()) - .key(dst.0.clone()) - .copy_source(format!("/{}/{}", self.bucket.to_string(), src.0.clone())) - .send() - .await; - - match maybe_copy { - Err(e) => { - tracing::error!( - "unable to copy object {} to {} (bucket: {}), error: {}", - src.0, - dst.0, - self.bucket, - e - ); - Err(StorageError::Internal) - } - Ok(_) => { - tracing::debug!("copied {} to {} (bucket: {})", src.0, dst.0, self.bucket); - Ok(()) - } - } - } - async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError> { - tracing::trace!(prefix = prefix, command = "blob_list"); - let maybe_list = self - .s3 - .list_objects_v2() - .bucket(self.bucket.to_string()) - .prefix(prefix) - .into_paginator() - .send() - .try_collect() - .await; - - match maybe_list { - Err(e) => { - tracing::error!( - "listing prefix {} on bucket {} failed: {}", - prefix, - self.bucket, - e - ); - Err(StorageError::Internal) - } - Ok(pagin_list_out) => Ok(pagin_list_out - .into_iter() - .map(|list_out| list_out.contents.unwrap_or(vec![])) - .flatten() - .map(|obj| BlobRef(obj.key.unwrap_or(String::new()))) - .collect::<Vec<_>>()), - } - } - async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError> { - tracing::trace!(entry=%blob_ref, command="blob_rm"); - let maybe_delete = self - .s3 - .delete_object() - .bucket(self.bucket.to_string()) - .key(blob_ref.0.clone()) - .send() - .await; - - match maybe_delete { - Err(e) => { - tracing::error!( - "unable to delete {} (bucket: {}), error {}", - blob_ref.0, - self.bucket, - e - ); - Err(StorageError::Internal) - } - Ok(_) => { - tracing::debug!("deleted {} (bucket: {})", blob_ref.0, self.bucket); - Ok(()) - } - } - } -} diff --git a/src/storage/in_memory.rs b/src/storage/in_memory.rs deleted file mode 100644 index 3c3a94c..0000000 --- a/src/storage/in_memory.rs +++ /dev/null @@ -1,334 +0,0 @@ -use crate::storage::*; -use std::collections::{BTreeMap, HashMap}; -use std::ops::Bound::{self, Excluded, Included, Unbounded}; -use std::sync::{Arc, RwLock}; -use tokio::sync::Notify; - -/// This implementation is very inneficient, and not completely correct -/// Indeed, when the connector is dropped, the memory is freed. -/// It means that when a user disconnects, its data are lost. -/// It's intended only for basic debugging, do not use it for advanced tests... - -#[derive(Debug, Default)] -pub struct MemDb(tokio::sync::Mutex<HashMap<String, Arc<MemBuilder>>>); -impl MemDb { - pub fn new() -> Self { - Self(tokio::sync::Mutex::new(HashMap::new())) - } - - pub async fn builder(&self, username: &str) -> Arc<MemBuilder> { - let mut global_storage = self.0.lock().await; - global_storage - .entry(username.to_string()) - .or_insert(MemBuilder::new(username)) - .clone() - } -} - -#[derive(Debug, Clone)] -enum InternalData { - Tombstone, - Value(Vec<u8>), -} -impl InternalData { - fn to_alternative(&self) -> Alternative { - match self { - Self::Tombstone => Alternative::Tombstone, - Self::Value(x) => Alternative::Value(x.clone()), - } - } -} - -#[derive(Debug)] -struct InternalRowVal { - data: Vec<InternalData>, - version: u64, - change: Arc<Notify>, -} -impl std::default::Default for InternalRowVal { - fn default() -> Self { - Self { - data: vec![], - version: 1, - change: Arc::new(Notify::new()), - } - } -} -impl InternalRowVal { - fn concurrent_values(&self) -> Vec<Alternative> { - self.data.iter().map(InternalData::to_alternative).collect() - } - - fn to_row_val(&self, row_ref: RowRef) -> RowVal { - RowVal { - row_ref: row_ref.with_causality(self.version.to_string()), - value: self.concurrent_values(), - } - } -} - -#[derive(Debug, Default, Clone)] -struct InternalBlobVal { - data: Vec<u8>, - metadata: HashMap<String, String>, -} -impl InternalBlobVal { - fn to_blob_val(&self, bref: &BlobRef) -> BlobVal { - BlobVal { - blob_ref: bref.clone(), - meta: self.metadata.clone(), - value: self.data.clone(), - } - } -} - -type ArcRow = Arc<RwLock<HashMap<String, BTreeMap<String, InternalRowVal>>>>; -type ArcBlob = Arc<RwLock<BTreeMap<String, InternalBlobVal>>>; - -#[derive(Clone, Debug)] -pub struct MemBuilder { - unicity: Vec<u8>, - row: ArcRow, - blob: ArcBlob, -} - -impl MemBuilder { - pub fn new(user: &str) -> Arc<Self> { - tracing::debug!("initialize membuilder for {}", user); - let mut unicity: Vec<u8> = vec![]; - unicity.extend_from_slice(file!().as_bytes()); - unicity.extend_from_slice(user.as_bytes()); - Arc::new(Self { - unicity, - row: Arc::new(RwLock::new(HashMap::new())), - blob: Arc::new(RwLock::new(BTreeMap::new())), - }) - } -} - -#[async_trait] -impl IBuilder for MemBuilder { - async fn build(&self) -> Result<Store, StorageError> { - Ok(Box::new(MemStore { - row: self.row.clone(), - blob: self.blob.clone(), - })) - } - - fn unique(&self) -> UnicityBuffer { - UnicityBuffer(self.unicity.clone()) - } -} - -pub struct MemStore { - row: ArcRow, - blob: ArcBlob, -} - -fn prefix_last_bound(prefix: &str) -> Bound<String> { - let mut sort_end = prefix.to_string(); - match sort_end.pop() { - None => Unbounded, - Some(ch) => { - let nc = char::from_u32(ch as u32 + 1).unwrap(); - sort_end.push(nc); - Excluded(sort_end) - } - } -} - -impl MemStore { - fn row_rm_single(&self, entry: &RowRef) -> Result<(), StorageError> { - tracing::trace!(entry=%entry, command="row_rm_single"); - let mut store = self.row.write().or(Err(StorageError::Internal))?; - let shard = &entry.uid.shard; - let sort = &entry.uid.sort; - - let cauz = match entry.causality.as_ref().map(|v| v.parse::<u64>()) { - Some(Ok(v)) => v, - _ => 0, - }; - - let bt = store.entry(shard.to_string()).or_default(); - let intval = bt.entry(sort.to_string()).or_default(); - - if cauz == intval.version { - intval.data.clear(); - } - intval.data.push(InternalData::Tombstone); - intval.version += 1; - intval.change.notify_waiters(); - - Ok(()) - } -} - -#[async_trait] -impl IStore for MemStore { - async fn row_fetch<'a>(&self, select: &Selector<'a>) -> Result<Vec<RowVal>, StorageError> { - tracing::trace!(select=%select, command="row_fetch"); - let store = self.row.read().or(Err(StorageError::Internal))?; - - match select { - Selector::Range { - shard, - sort_begin, - sort_end, - } => Ok(store - .get(*shard) - .unwrap_or(&BTreeMap::new()) - .range(( - Included(sort_begin.to_string()), - Excluded(sort_end.to_string()), - )) - .map(|(k, v)| v.to_row_val(RowRef::new(shard, k))) - .collect::<Vec<_>>()), - Selector::List(rlist) => { - let mut acc = vec![]; - for row_ref in rlist { - let maybe_intval = store - .get(&row_ref.uid.shard) - .map(|v| v.get(&row_ref.uid.sort)) - .flatten(); - if let Some(intval) = maybe_intval { - acc.push(intval.to_row_val(row_ref.clone())); - } - } - Ok(acc) - } - Selector::Prefix { shard, sort_prefix } => { - let last_bound = prefix_last_bound(sort_prefix); - - Ok(store - .get(*shard) - .unwrap_or(&BTreeMap::new()) - .range((Included(sort_prefix.to_string()), last_bound)) - .map(|(k, v)| v.to_row_val(RowRef::new(shard, k))) - .collect::<Vec<_>>()) - } - Selector::Single(row_ref) => { - let intval = store - .get(&row_ref.uid.shard) - .ok_or(StorageError::NotFound)? - .get(&row_ref.uid.sort) - .ok_or(StorageError::NotFound)?; - Ok(vec![intval.to_row_val((*row_ref).clone())]) - } - } - } - - async fn row_rm<'a>(&self, select: &Selector<'a>) -> Result<(), StorageError> { - tracing::trace!(select=%select, command="row_rm"); - - let values = match select { - Selector::Range { .. } | Selector::Prefix { .. } => self - .row_fetch(select) - .await? - .into_iter() - .map(|rv| rv.row_ref) - .collect::<Vec<_>>(), - Selector::List(rlist) => rlist.clone(), - Selector::Single(row_ref) => vec![(*row_ref).clone()], - }; - - for v in values.into_iter() { - self.row_rm_single(&v)?; - } - Ok(()) - } - - async fn row_insert(&self, values: Vec<RowVal>) -> Result<(), StorageError> { - tracing::trace!(entries=%values.iter().map(|v| v.row_ref.to_string()).collect::<Vec<_>>().join(","), command="row_insert"); - let mut store = self.row.write().or(Err(StorageError::Internal))?; - for v in values.into_iter() { - let shard = v.row_ref.uid.shard; - let sort = v.row_ref.uid.sort; - - let val = match v.value.into_iter().next() { - Some(Alternative::Value(x)) => x, - _ => vec![], - }; - - let cauz = match v.row_ref.causality.map(|v| v.parse::<u64>()) { - Some(Ok(v)) => v, - _ => 0, - }; - - let bt = store.entry(shard).or_default(); - let intval = bt.entry(sort).or_default(); - - if cauz == intval.version { - intval.data.clear(); - } - intval.data.push(InternalData::Value(val)); - intval.version += 1; - intval.change.notify_waiters(); - } - Ok(()) - } - async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError> { - tracing::trace!(entry=%value, command="row_poll"); - let shard = &value.uid.shard; - let sort = &value.uid.sort; - let cauz = match value.causality.as_ref().map(|v| v.parse::<u64>()) { - Some(Ok(v)) => v, - _ => 0, - }; - - let notify_me = { - let mut store = self.row.write().or(Err(StorageError::Internal))?; - let bt = store.entry(shard.to_string()).or_default(); - let intval = bt.entry(sort.to_string()).or_default(); - - if intval.version != cauz { - return Ok(intval.to_row_val(value.clone())); - } - intval.change.clone() - }; - - notify_me.notified().await; - - let res = self.row_fetch(&Selector::Single(value)).await?; - res.into_iter().next().ok_or(StorageError::NotFound) - } - - async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError> { - tracing::trace!(entry=%blob_ref, command="blob_fetch"); - let store = self.blob.read().or(Err(StorageError::Internal))?; - store - .get(&blob_ref.0) - .ok_or(StorageError::NotFound) - .map(|v| v.to_blob_val(blob_ref)) - } - async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> { - tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert"); - let mut store = self.blob.write().or(Err(StorageError::Internal))?; - let entry = store.entry(blob_val.blob_ref.0.clone()).or_default(); - entry.data = blob_val.value.clone(); - entry.metadata = blob_val.meta.clone(); - Ok(()) - } - async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> { - tracing::trace!(src=%src, dst=%dst, command="blob_copy"); - let mut store = self.blob.write().or(Err(StorageError::Internal))?; - let blob_src = store.entry(src.0.clone()).or_default().clone(); - store.insert(dst.0.clone(), blob_src); - Ok(()) - } - async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError> { - tracing::trace!(prefix = prefix, command = "blob_list"); - let store = self.blob.read().or(Err(StorageError::Internal))?; - let last_bound = prefix_last_bound(prefix); - let blist = store - .range((Included(prefix.to_string()), last_bound)) - .map(|(k, _)| BlobRef(k.to_string())) - .collect::<Vec<_>>(); - Ok(blist) - } - async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError> { - tracing::trace!(entry=%blob_ref, command="blob_rm"); - let mut store = self.blob.write().or(Err(StorageError::Internal))?; - store.remove(&blob_ref.0); - Ok(()) - } -} diff --git a/src/storage/mod.rs b/src/storage/mod.rs deleted file mode 100644 index 1f86f71..0000000 --- a/src/storage/mod.rs +++ /dev/null @@ -1,179 +0,0 @@ -/* - * - * This abstraction goal is to leverage all the semantic of Garage K2V+S3, - * to be as tailored as possible to it ; it aims to be a zero-cost abstraction - * compared to when we where directly using the K2V+S3 client. - * - * My idea: we can encapsulate the causality token - * into the object system so it is not exposed. - */ - -pub mod garage; -pub mod in_memory; - -use async_trait::async_trait; -use std::collections::HashMap; -use std::hash::Hash; -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub enum Alternative { - Tombstone, - Value(Vec<u8>), -} -type ConcurrentValues = Vec<Alternative>; - -#[derive(Debug, Clone)] -pub enum StorageError { - NotFound, - Internal, -} -impl std::fmt::Display for StorageError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Storage Error: ")?; - match self { - Self::NotFound => f.write_str("Item not found"), - Self::Internal => f.write_str("An internal error occured"), - } - } -} -impl std::error::Error for StorageError {} - -#[derive(Debug, Clone, PartialEq)] -pub struct RowUid { - pub shard: String, - pub sort: String, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct RowRef { - pub uid: RowUid, - pub causality: Option<String>, -} -impl std::fmt::Display for RowRef { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "RowRef({}, {}, {:?})", - self.uid.shard, self.uid.sort, self.causality - ) - } -} - -impl RowRef { - pub fn new(shard: &str, sort: &str) -> Self { - Self { - uid: RowUid { - shard: shard.to_string(), - sort: sort.to_string(), - }, - causality: None, - } - } - pub fn with_causality(mut self, causality: String) -> Self { - self.causality = Some(causality); - self - } -} - -#[derive(Debug, Clone)] -pub struct RowVal { - pub row_ref: RowRef, - pub value: ConcurrentValues, -} - -impl RowVal { - pub fn new(row_ref: RowRef, value: Vec<u8>) -> Self { - Self { - row_ref, - value: vec![Alternative::Value(value)], - } - } -} - -#[derive(Debug, Clone)] -pub struct BlobRef(pub String); -impl std::fmt::Display for BlobRef { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "BlobRef({})", self.0) - } -} - -#[derive(Debug, Clone)] -pub struct BlobVal { - pub blob_ref: BlobRef, - pub meta: HashMap<String, String>, - pub value: Vec<u8>, -} -impl BlobVal { - pub fn new(blob_ref: BlobRef, value: Vec<u8>) -> Self { - Self { - blob_ref, - value, - meta: HashMap::new(), - } - } - - pub fn with_meta(mut self, k: String, v: String) -> Self { - self.meta.insert(k, v); - self - } -} - -#[derive(Debug)] -pub enum Selector<'a> { - Range { - shard: &'a str, - sort_begin: &'a str, - sort_end: &'a str, - }, - List(Vec<RowRef>), // list of (shard_key, sort_key) - #[allow(dead_code)] - Prefix { - shard: &'a str, - sort_prefix: &'a str, - }, - Single(&'a RowRef), -} -impl<'a> std::fmt::Display for Selector<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Range { - shard, - sort_begin, - sort_end, - } => write!(f, "Range({}, [{}, {}[)", shard, sort_begin, sort_end), - Self::List(list) => write!(f, "List({:?})", list), - Self::Prefix { shard, sort_prefix } => write!(f, "Prefix({}, {})", shard, sort_prefix), - Self::Single(row_ref) => write!(f, "Single({})", row_ref), - } - } -} - -#[async_trait] -pub trait IStore { - async fn row_fetch<'a>(&self, select: &Selector<'a>) -> Result<Vec<RowVal>, StorageError>; - async fn row_rm<'a>(&self, select: &Selector<'a>) -> Result<(), StorageError>; - async fn row_insert(&self, values: Vec<RowVal>) -> Result<(), StorageError>; - async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError>; - - async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError>; - async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError>; - async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError>; - async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError>; - async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError>; -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct UnicityBuffer(Vec<u8>); - -#[async_trait] -pub trait IBuilder: std::fmt::Debug { - async fn build(&self) -> Result<Store, StorageError>; - - /// Returns an opaque buffer that uniquely identifies this builder - fn unique(&self) -> UnicityBuffer; -} - -pub type Builder = Arc<dyn IBuilder + Send + Sync>; -pub type Store = Box<dyn IStore + Send + Sync>; diff --git a/src/timestamp.rs b/src/timestamp.rs deleted file mode 100644 index 76cb74b..0000000 --- a/src/timestamp.rs +++ /dev/null @@ -1,65 +0,0 @@ -use rand::prelude::*; -use std::str::FromStr; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// Returns milliseconds since UNIX Epoch -pub fn now_msec() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Fix your clock :o") - .as_millis() as u64 -} - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub struct Timestamp { - pub msec: u64, - pub rand: u64, -} - -impl Timestamp { - #[allow(dead_code)] - // 2023-05-15 try to make clippy happy and not sure if this fn will be used in the future. - pub fn now() -> Self { - let mut rng = thread_rng(); - Self { - msec: now_msec(), - rand: rng.gen::<u64>(), - } - } - - pub fn after(other: &Self) -> Self { - let mut rng = thread_rng(); - Self { - msec: std::cmp::max(now_msec(), other.msec + 1), - rand: rng.gen::<u64>(), - } - } - - pub fn zero() -> Self { - Self { msec: 0, rand: 0 } - } -} - -impl ToString for Timestamp { - fn to_string(&self) -> String { - let mut bytes = [0u8; 16]; - bytes[0..8].copy_from_slice(&u64::to_be_bytes(self.msec)); - bytes[8..16].copy_from_slice(&u64::to_be_bytes(self.rand)); - hex::encode(bytes) - } -} - -impl FromStr for Timestamp { - type Err = &'static str; - - fn from_str(s: &str) -> Result<Timestamp, &'static str> { - let bytes = hex::decode(s).map_err(|_| "invalid hex")?; - if bytes.len() != 16 { - return Err("bad length"); - } - Ok(Self { - msec: u64::from_be_bytes(bytes[0..8].try_into().unwrap()), - rand: u64::from_be_bytes(bytes[8..16].try_into().unwrap()), - }) - } -} diff --git a/src/user.rs b/src/user.rs deleted file mode 100644 index a38b9c1..0000000 --- a/src/user.rs +++ /dev/null @@ -1,313 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; -use std::sync::{Arc, Weak}; - -use anyhow::{anyhow, bail, Result}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use tokio::sync::watch; - -use crate::cryptoblob::{open_deserialize, seal_serialize}; -use crate::login::Credentials; -use crate::mail::incoming::incoming_mail_watch_process; -use crate::mail::mailbox::Mailbox; -use crate::mail::uidindex::ImapUidvalidity; -use crate::mail::unique_ident::{gen_ident, UniqueIdent}; -use crate::storage; -use crate::timestamp::now_msec; - -use crate::mail::namespace::{MAILBOX_HIERARCHY_DELIMITER, INBOX, DRAFTS, ARCHIVE, SENT, TRASH, MAILBOX_LIST_PK, MAILBOX_LIST_SK,MailboxList,CreatedMailbox}; - -//@FIXME User should be totally rewriten -//to extract the local mailbox list -//to the mail/namespace.rs file (and mailbox list should be reworded as mail namespace) - -pub struct User { - pub username: String, - pub creds: Credentials, - pub storage: storage::Store, - pub mailboxes: std::sync::Mutex<HashMap<UniqueIdent, Weak<Mailbox>>>, - - tx_inbox_id: watch::Sender<Option<(UniqueIdent, ImapUidvalidity)>>, -} - -impl User { - pub async fn new(username: String, creds: Credentials) -> Result<Arc<Self>> { - let cache_key = (username.clone(), creds.storage.unique()); - - { - let cache = USER_CACHE.lock().unwrap(); - if let Some(u) = cache.get(&cache_key).and_then(Weak::upgrade) { - return Ok(u); - } - } - - let user = Self::open(username, creds).await?; - - let mut cache = USER_CACHE.lock().unwrap(); - if let Some(concurrent_user) = cache.get(&cache_key).and_then(Weak::upgrade) { - drop(user); - Ok(concurrent_user) - } else { - cache.insert(cache_key, Arc::downgrade(&user)); - Ok(user) - } - } - - /// Lists user's available mailboxes - pub async fn list_mailboxes(&self) -> Result<Vec<String>> { - let (list, _ct) = self.load_mailbox_list().await?; - Ok(list.existing_mailbox_names()) - } - - /// Opens an existing mailbox given its IMAP name. - pub async fn open_mailbox(&self, name: &str) -> Result<Option<Arc<Mailbox>>> { - let (mut list, ct) = self.load_mailbox_list().await?; - - //@FIXME it could be a trace or an opentelemtry trace thing. - // Be careful to not leak sensible data - /* - eprintln!("List of mailboxes:"); - for ent in list.0.iter() { - eprintln!(" - {:?}", ent); - } - */ - - if let Some((uidvalidity, Some(mbid))) = list.get_mailbox(name) { - let mb = self.open_mailbox_by_id(mbid, uidvalidity).await?; - let mb_uidvalidity = mb.current_uid_index().await.uidvalidity; - if mb_uidvalidity > uidvalidity { - list.update_uidvalidity(name, mb_uidvalidity); - self.save_mailbox_list(&list, ct).await?; - } - Ok(Some(mb)) - } else { - Ok(None) - } - } - - /// Check whether mailbox exists - pub async fn has_mailbox(&self, name: &str) -> Result<bool> { - let (list, _ct) = self.load_mailbox_list().await?; - Ok(list.has_mailbox(name)) - } - - /// Creates a new mailbox in the user's IMAP namespace. - pub async fn create_mailbox(&self, name: &str) -> Result<()> { - if name.ends_with(MAILBOX_HIERARCHY_DELIMITER) { - bail!("Invalid mailbox name: {}", name); - } - - let (mut list, ct) = self.load_mailbox_list().await?; - match list.create_mailbox(name) { - CreatedMailbox::Created(_, _) => { - self.save_mailbox_list(&list, ct).await?; - Ok(()) - } - CreatedMailbox::Existed(_, _) => Err(anyhow!("Mailbox {} already exists", name)), - } - } - - /// Deletes a mailbox in the user's IMAP namespace. - pub async fn delete_mailbox(&self, name: &str) -> Result<()> { - if name == INBOX { - bail!("Cannot delete INBOX"); - } - - let (mut list, ct) = self.load_mailbox_list().await?; - if list.has_mailbox(name) { - //@TODO: actually delete mailbox contents - list.set_mailbox(name, None); - self.save_mailbox_list(&list, ct).await?; - Ok(()) - } else { - bail!("Mailbox {} does not exist", name); - } - } - - /// Renames a mailbox in the user's IMAP namespace. - pub async fn rename_mailbox(&self, old_name: &str, new_name: &str) -> Result<()> { - let (mut list, ct) = self.load_mailbox_list().await?; - - if old_name.ends_with(MAILBOX_HIERARCHY_DELIMITER) { - bail!("Invalid mailbox name: {}", old_name); - } - if new_name.ends_with(MAILBOX_HIERARCHY_DELIMITER) { - bail!("Invalid mailbox name: {}", new_name); - } - - if old_name == INBOX { - list.rename_mailbox(old_name, new_name)?; - if !self.ensure_inbox_exists(&mut list, &ct).await? { - self.save_mailbox_list(&list, ct).await?; - } - } else { - let names = list.existing_mailbox_names(); - - let old_name_w_delim = format!("{}{}", old_name, MAILBOX_HIERARCHY_DELIMITER); - let new_name_w_delim = format!("{}{}", new_name, MAILBOX_HIERARCHY_DELIMITER); - - if names - .iter() - .any(|x| x == new_name || x.starts_with(&new_name_w_delim)) - { - bail!("Mailbox {} already exists", new_name); - } - - for name in names.iter() { - if name == old_name { - list.rename_mailbox(name, new_name)?; - } else if let Some(tail) = name.strip_prefix(&old_name_w_delim) { - let nnew = format!("{}{}", new_name_w_delim, tail); - list.rename_mailbox(name, &nnew)?; - } - } - - self.save_mailbox_list(&list, ct).await?; - } - Ok(()) - } - - // ---- Internal user & mailbox management ---- - - async fn open(username: String, creds: Credentials) -> Result<Arc<Self>> { - let storage = creds.storage.build().await?; - - let (tx_inbox_id, rx_inbox_id) = watch::channel(None); - - let user = Arc::new(Self { - username, - creds: creds.clone(), - storage, - tx_inbox_id, - mailboxes: std::sync::Mutex::new(HashMap::new()), - }); - - // Ensure INBOX exists (done inside load_mailbox_list) - user.load_mailbox_list().await?; - - tokio::spawn(incoming_mail_watch_process( - Arc::downgrade(&user), - user.creds.clone(), - rx_inbox_id, - )); - - Ok(user) - } - - pub(super) async fn open_mailbox_by_id( - &self, - id: UniqueIdent, - min_uidvalidity: ImapUidvalidity, - ) -> Result<Arc<Mailbox>> { - { - let cache = self.mailboxes.lock().unwrap(); - if let Some(mb) = cache.get(&id).and_then(Weak::upgrade) { - return Ok(mb); - } - } - - let mb = Arc::new(Mailbox::open(&self.creds, id, min_uidvalidity).await?); - - let mut cache = self.mailboxes.lock().unwrap(); - if let Some(concurrent_mb) = cache.get(&id).and_then(Weak::upgrade) { - drop(mb); // we worked for nothing but at least we didn't starve someone else - Ok(concurrent_mb) - } else { - cache.insert(id, Arc::downgrade(&mb)); - Ok(mb) - } - } - - // ---- Mailbox list management ---- - - async fn load_mailbox_list(&self) -> Result<(MailboxList, Option<storage::RowRef>)> { - let row_ref = storage::RowRef::new(MAILBOX_LIST_PK, MAILBOX_LIST_SK); - let (mut list, row) = match self - .storage - .row_fetch(&storage::Selector::Single(&row_ref)) - .await - { - Err(storage::StorageError::NotFound) => (MailboxList::new(), None), - Err(e) => return Err(e.into()), - Ok(rv) => { - let mut list = MailboxList::new(); - let (row_ref, row_vals) = match rv.into_iter().next() { - Some(row_val) => (row_val.row_ref, row_val.value), - None => (row_ref, vec![]), - }; - - for v in row_vals { - if let storage::Alternative::Value(vbytes) = v { - let list2 = - open_deserialize::<MailboxList>(&vbytes, &self.creds.keys.master)?; - list.merge(list2); - } - } - (list, Some(row_ref)) - } - }; - - let is_default_mbx_missing = [DRAFTS, ARCHIVE, SENT, TRASH] - .iter() - .map(|mbx| list.create_mailbox(mbx)) - .fold(false, |acc, r| { - acc || matches!(r, CreatedMailbox::Created(..)) - }); - let is_inbox_missing = self.ensure_inbox_exists(&mut list, &row).await?; - if is_default_mbx_missing && !is_inbox_missing { - // It's the only case where we created some mailboxes and not saved them - // So we save them! - self.save_mailbox_list(&list, row.clone()).await?; - } - - Ok((list, row)) - } - - async fn ensure_inbox_exists( - &self, - list: &mut MailboxList, - ct: &Option<storage::RowRef>, - ) -> Result<bool> { - // If INBOX doesn't exist, create a new mailbox with that name - // and save new mailbox list. - // Also, ensure that the mpsc::watch that keeps track of the - // inbox id is up-to-date. - let saved; - let (inbox_id, inbox_uidvalidity) = match list.create_mailbox(INBOX) { - CreatedMailbox::Created(i, v) => { - self.save_mailbox_list(list, ct.clone()).await?; - saved = true; - (i, v) - } - CreatedMailbox::Existed(i, v) => { - saved = false; - (i, v) - } - }; - let inbox_id = Some((inbox_id, inbox_uidvalidity)); - if *self.tx_inbox_id.borrow() != inbox_id { - self.tx_inbox_id.send(inbox_id).unwrap(); - } - - Ok(saved) - } - - async fn save_mailbox_list( - &self, - list: &MailboxList, - ct: Option<storage::RowRef>, - ) -> Result<()> { - let list_blob = seal_serialize(list, &self.creds.keys.master)?; - let rref = ct.unwrap_or(storage::RowRef::new(MAILBOX_LIST_PK, MAILBOX_LIST_SK)); - let row_val = storage::RowVal::new(rref, list_blob); - self.storage.row_insert(vec![row_val]).await?; - Ok(()) - } -} - -// ---- User cache ---- - -lazy_static! { - static ref USER_CACHE: std::sync::Mutex<HashMap<(String, storage::UnicityBuffer), Weak<User>>> = - std::sync::Mutex::new(HashMap::new()); -} |