diff --git a/src/ldap/client_state.rs b/src/ldap/client_state.rs index 6ee2d21..399821b 100644 --- a/src/ldap/client_state.rs +++ b/src/ldap/client_state.rs @@ -1,4 +1,4 @@ -use crate::ldap::Dn; +use crate::ldap::BindDn; /// Whether a client is successfully logged in (bound). /// @@ -6,7 +6,7 @@ use crate::ldap::Dn; #[derive(Clone, Debug)] pub enum LdapClientState { Unbound, - Bound(Dn), + Bound(BindDn), } impl LdapClientState { @@ -18,11 +18,11 @@ impl LdapClientState { *self = Self::Unbound; } - pub fn bind(&mut self, dn: Dn) { + pub fn bind(&mut self, dn: BindDn) { *self = Self::Bound(dn); } - pub fn bound_dn(&self) -> Option<&Dn> { + pub fn bound_dn(&self) -> Option<&BindDn> { match self { Self::Unbound => None, Self::Bound(dn) => Some(dn), @@ -30,6 +30,6 @@ impl LdapClientState { } pub fn bound_dn_string(&self) -> String { - self.bound_dn().map_or(String::new(), Dn::to_dn_string) + self.bound_dn().map_or(String::new(), BindDn::to_dn_string) } } diff --git a/src/ldap/dn.rs b/src/ldap/dn.rs index d431572..87922aa 100644 --- a/src/ldap/dn.rs +++ b/src/ldap/dn.rs @@ -1,8 +1,6 @@ use dn_escape::dn_escape; -use ldap3_proto::LdapResultCode; -use crate::db::UserRef; -use crate::ldap::LdapReturnError; +use crate::db::User; /// A simple multi-value Map powered by a vector, to respect /// order of first appearance of keys. @@ -52,30 +50,7 @@ impl VecMap { } #[derive(Clone, Debug)] -pub struct InvalidDnError(pub String); - -impl LdapReturnError for InvalidDnError { - fn code(&self) -> LdapResultCode { - LdapResultCode::InvalidDNSyntax - } - - fn message(&self) -> String { - format!("Invalid dn: {}", self.0) - } -} - -#[derive(Clone, Debug)] -pub struct NotUserDnError(pub String); - -impl LdapReturnError for NotUserDnError { - fn code(&self) -> LdapResultCode { - LdapResultCode::InvalidDNSyntax - } - - fn message(&self) -> String { - format!("Not a user dn containing uid/dc: {}", self.0) - } -} +pub struct MalformedDn; /// A Dn is a key-value mapping which can contain the same key several times. /// @@ -96,7 +71,7 @@ impl Dn { /// /// However, if we find a really funny request such as `dc=foo=bar`, then /// we return an error to the client. - pub fn from_dn_str(input: &str) -> Result { + pub fn from_dn_str(input: &str) -> Result { let mut keys = VecMap::new(); if input.is_empty() { @@ -108,7 +83,7 @@ impl Dn { // Here we have key=val pairs if query_parts.clone().count() != 2 { // Bad request - return Err(InvalidDnError(input.to_string())); + return Err(MalformedDn); } let key = query_parts.next().unwrap(); @@ -169,21 +144,15 @@ impl Dn { self.keys.insert("dc", domain_components, force); } - pub fn to_user_ref(&self) -> Result { - let Some(username_vals) = self.keys.get("uid") else { - return Err(NotUserDnError(self.to_dn_string())); - }; - if username_vals.len() != 1 { - return Err(NotUserDnError(self.to_dn_string())); + pub fn from_user(user: &User) -> Self { + let mut keys = VecMap::new(); + keys.insert_or_append("uid", &user.username); + keys.insert_or_append("ou", "people"); + for domain_component in user.domain.split('.') { + keys.insert_or_append("dc", domain_component); } - let username = username_vals[0].clone(); - let Some(domain_vals) = self.keys.get("dc") else { - return Err(NotUserDnError(self.to_dn_string())); - }; - let domain = domain_vals.join("."); - - Ok(UserRef { username, domain }) + Self { keys } } } diff --git a/src/ldap/filter/mail.rs b/src/ldap/filter/mail.rs new file mode 100644 index 0000000..0f17ac0 --- /dev/null +++ b/src/ldap/filter/mail.rs @@ -0,0 +1,248 @@ +use ldap3_proto::{LdapFilter, LdapResultCode}; + +use std::fmt; + +use crate::db::UserRef; +use crate::ldap::LdapReturnError; + +#[derive(Clone, Debug)] +pub struct MailFilter { + pub complete: String, + pub username: String, + pub domain: String, +} + +impl fmt::Display for MailFilter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.complete) + } +} + +impl MailFilter { + /// Tries to parse a string into an email address. + /// + /// This method is not really RFC-compliant because it merely splits by `@`, + /// but is good enough for what we do. + pub fn new(value: &str) -> Result { + let original = value; + // Here we normalize the mail attribute to lowercase to prevent + // useless mismatches. + // TODO: investigate if that's supposed to be a problem for anyone? + let value = value.to_lowercase(); + if value != original { + tracing::debug!("Normalized search email from {original} to {value}"); + } + + let mut parts = value.split('@'); + let username = parts.next().unwrap(); + let Some(domain) = parts.next() else { + tracing::debug!("Not a valid username@domain email: {value}"); + return Err(MailDomainError::InvalidMail); + }; + + if parts.next().is_some() { + tracing::debug!("Too many parts in mail address"); + return Err(MailDomainError::InvalidMail); + } + + Ok(Self { + complete: value.clone(), + username: username.to_string(), + domain: domain.to_string(), + }) + } + + /// Extract any mail filter from an LDAP search filter + /// which may contain other criteria, which we overall don't care about + /// at the moment. + pub fn from_search_filter(filter: &LdapFilter) -> Result { + let res = match filter { + LdapFilter::And(filters) | LdapFilter::Or(filters) => { + Self::from_multiple_filters(filters) + } + LdapFilter::Equality(attr, value) => { + if let Some(domain) = Self::from_equality_filter(attr, value)? { + Ok(domain) + } else { + Err(MailDomainError::NoMailFilter) + } + } + _ => Err(MailDomainError::InvalidFilter), + }; + + match &res { + Ok(mail) => tracing::debug!("Found email in search filter: {mail}"), + Err(e) => tracing::debug!("Not found email in search filter: {}", e.message()), + } + + res + } + + /// Extract any mail filter from a bunch of LDAP filters. + /// + /// Any filter that is not an equality check is discarded. + fn from_multiple_filters(filters: &[LdapFilter]) -> Result { + for filter in filters { + if let LdapFilter::Equality(attr, value) = filter { + // Here if we receive None, it means the filter was not checking for the + // `mail` attr so we continue iterating. + if let Some(found) = Self::from_equality_filter(attr, value)? { + return Ok(found); + } + } + } + + Err(MailDomainError::NoMailFilter) + } + + /// Extract any mail filter from an LDAP search equality filter extracted + /// from a global search filter. + fn from_equality_filter(attr: &str, value: &str) -> Result, MailDomainError> { + if attr != "mail" { + return Ok(None); + } + + let mail = Self::new(value)?; + Ok(Some(mail)) + } + + pub fn to_user_ref(&self) -> UserRef { + UserRef { + username: self.username.clone(), + domain: self.domain.clone(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum MailDomainError { + /// mail value not user@domain format + InvalidMail, + /// No mail filter found in the search query + NoMailFilter, + /// Filter is not And/Or/Equality for which we can find a mail filter + InvalidFilter, +} + +impl fmt::Display for MailDomainError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let msg = match self { + Self::InvalidMail => { + "No valid email address requested in mail filter in search request" + } + Self::NoMailFilter => "No mail filter found in search request", + Self::InvalidFilter => "No AND/OR/EQUALITY filter found in search request", + }; + write!(f, "{msg}") + } +} + +impl LdapReturnError for MailDomainError { + fn code(&self) -> LdapResultCode { + match self { + Self::InvalidMail => LdapResultCode::InvalidAttributeSyntax, + Self::NoMailFilter => LdapResultCode::InappropriateMatching, + Self::InvalidFilter => LdapResultCode::UnwillingToPerform, + } + } + + fn message(&self) -> String { + self.to_string() + } +} + +#[cfg(test)] +mod tests { + use crate::filter::{search_filter_and, search_filter_eq}; + + use super::*; + + #[test] + fn no_mail_filter() { + let filter = search_filter_and(&[ + search_filter_eq("uid", "a"), + search_filter_eq("objectClass", "inetOrgPerson"), + ]); + + let mail_filter = MailFilter::from_search_filter(&filter); + println!("{:?}", mail_filter); + let e = mail_filter.unwrap_err(); + assert_eq!(e, MailDomainError::NoMailFilter); + } + + #[test] + fn invalid_filter() { + let filter = LdapFilter::Approx("mail".to_string(), "a@a.localhost".to_string()); + let mail_filter = MailFilter::from_search_filter(&filter); + println!("{:?}", mail_filter); + let e = mail_filter.unwrap_err(); + assert_eq!(e, MailDomainError::InvalidFilter); + } + + #[test] + fn invalid_mail_no_domain() { + let filter = search_filter_eq("mail", "a"); + let mail_filter = MailFilter::from_search_filter(&filter); + println!("{:?}", mail_filter); + let e = mail_filter.unwrap_err(); + assert_eq!(e, MailDomainError::InvalidMail); + } + + #[test] + fn invalid_mail_too_many_parts() { + let filter = search_filter_eq("mail", "a@a.localhost@a.localhost"); + let mail_filter = MailFilter::from_search_filter(&filter); + println!("{:?}", mail_filter); + let e = mail_filter.unwrap_err(); + assert_eq!(e, MailDomainError::InvalidMail); + } + + #[test] + fn valid_basic() { + let filter = search_filter_eq("mail", "a@a.localhost"); + let mail_filter = MailFilter::from_search_filter(&filter); + println!("{:?}", mail_filter); + let mail = mail_filter.unwrap(); + assert_eq!(mail.complete, "a@a.localhost"); + assert_eq!(mail.username, "a"); + assert_eq!(mail.domain, "a.localhost"); + } + + #[test] + fn valid_basic_normalization() { + let filter = search_filter_eq("mail", "A@A.localhost"); + let mail_filter = MailFilter::from_search_filter(&filter); + println!("{:?}", mail_filter); + let mail = mail_filter.unwrap(); + assert_eq!(mail.complete, "a@a.localhost"); + assert_eq!(mail.username, "a"); + assert_eq!(mail.domain, "a.localhost"); + } + + #[test] + fn stalwart_default() { + // (&(objectClass=inetOrgPerson)(mail=?)) + let filter = search_filter_and(&[ + search_filter_eq("mail", "a@a.localhost"), + search_filter_eq("objectClass", "inetOrgPerson"), + ]); + + let mail_filter = MailFilter::from_search_filter(&filter); + println!("{:?}", mail_filter); + let mail = mail_filter.unwrap(); + assert_eq!(mail.complete, "a@a.localhost"); + assert_eq!(mail.username, "a"); + assert_eq!(mail.domain, "a.localhost"); + } + + // fn stalwart_lldap_example() { + // // &(|(objectClass=person)(member=cn=mail,ou=groups,dc=example,dc=org))(uid=?)) + // let filter = search_filter_and(&[ + // search_filter_eq("uid", "a"), + // search_filter_or(&[ + // search_filter_eq("objectClass", "person"), + // search_filter_eq("member", "cn=mail,ou=groups,dc=a,dc=localhost"), + // ]), + // ]); + // } +} diff --git a/src/ldap/filter/mod.rs b/src/ldap/filter/mod.rs new file mode 100644 index 0000000..f764e1c --- /dev/null +++ b/src/ldap/filter/mod.rs @@ -0,0 +1 @@ +pub mod mail; diff --git a/src/ldap/handler.rs b/src/ldap/handler.rs index e1a8710..32d2ac0 100644 --- a/src/ldap/handler.rs +++ b/src/ldap/handler.rs @@ -2,7 +2,9 @@ use ldap3_proto::LdapMsg; use ldap3_proto::proto::LdapOp; use crate::db::{Database, DatabaseInterface}; -use crate::ldap::{LdapClientState, LdapStream, LdapStreamError, op_bind, op_ext}; +use crate::ldap::{ + LdapClientState, LdapStream, LdapStreamError, op_bind, op_ext, search_by_mail_filter, +}; #[tracing::instrument(name = "ldap", skip(stream, db), fields(session = %stream.session))] pub async fn ldap_handler(mut stream: LdapStream, mut db: Database) { @@ -79,11 +81,21 @@ pub async fn ldap_handler_inner( client_state.bind(bound_dn); Ok(true) } else { - tracing::debug!("Unsuccessful bind"); - // TODO: abort connection here? - Ok(false) + client_state.unbind(); + tracing::debug!("Failed bind or anonymous bind"); + // We keep the connection open in case it's an anonymous bind + Ok(true) } } + LdapMsg { + msgid, + op: LdapOp::SearchRequest(sr), + // TODO: ctrl for pagination + ctrl: _, + } => { + search_by_mail_filter(stream, db, sr, msgid).await?; + Ok(true) + } // Unsupported message _ => { tracing::warn!("Unsupported client message, closing connection"); diff --git a/src/ldap/mod.rs b/src/ldap/mod.rs index c26dcf9..d72d094 100644 --- a/src/ldap/mod.rs +++ b/src/ldap/mod.rs @@ -1,12 +1,14 @@ mod client_state; pub use client_state::LdapClientState; mod dn; -pub use dn::{Dn, InvalidDnError, NotUserDnError}; +pub use dn::{Dn, MalformedDn}; +mod filter; mod handler; mod op; pub use handler::ldap_handler; -pub use op::bind::op_bind; +pub use op::bind::{BindDn, op_bind}; pub use op::ext::op_ext; +pub use op::search::search_by_mail_filter; mod return_error; pub use return_error::LdapReturnError; mod stream; diff --git a/src/ldap/op/bind.rs b/src/ldap/op/bind.rs index 8aee215..5b44c41 100644 --- a/src/ldap/op/bind.rs +++ b/src/ldap/op/bind.rs @@ -2,17 +2,91 @@ use ldap3_proto::proto::{LdapBindCred, LdapBindRequest, LdapBindResponse, LdapOp use ldap3_proto::{LdapMsg, LdapResultCode}; use crate::db::error::BoxedError; -use crate::db::{Database, DatabaseInterface}; -use crate::ldap::{ - Dn, InvalidDnError, LdapReturnError, LdapStream, LdapStreamError, NotUserDnError, -}; +use crate::db::{Database, DatabaseInterface, UserRef}; +use crate::ldap::{Dn, LdapReturnError, LdapStream, LdapStreamError, MalformedDn}; + +#[derive(Debug)] +pub struct InvalidBindDn { + dn: String, + kind: InvalidBindDnKind, +} + +#[derive(Debug)] +pub enum InvalidBindDnKind { + Malformed(MalformedDn), + NoUid, + MultipleUid, + NoDc, +} + +impl LdapReturnError for InvalidBindDn { + fn code(&self) -> LdapResultCode { + LdapResultCode::InvalidDNSyntax + } + + fn message(&self) -> String { + match &self.kind { + InvalidBindDnKind::Malformed(_e) => format!("Malformed dn: {}", self.dn), + InvalidBindDnKind::NoUid => format!("Missing `uid` in bind dn: {}", self.dn), + InvalidBindDnKind::MultipleUid => { + format!("Multiple `uid` accounts provided in bind dn: {}", self.dn) + } + InvalidBindDnKind::NoDc => format!("Missing `dc` in bind dn: {}", self.dn), + } + } +} + +#[derive(Clone, Debug)] +pub struct BindDn(Dn); + +impl BindDn { + pub fn from_dn_str(input: &str) -> Result { + let dn = Dn::from_dn_str(input).map_err(|e| InvalidBindDn { + dn: input.to_string(), + kind: InvalidBindDnKind::Malformed(e), + })?; + + let Some(uid) = dn.keys.get("uid") else { + return Err(InvalidBindDn { + dn: input.to_string(), + kind: InvalidBindDnKind::NoUid, + }); + }; + + if uid.len() != 1 { + return Err(InvalidBindDn { + dn: input.to_string(), + kind: InvalidBindDnKind::MultipleUid, + }); + } + + if dn.get_hostname().is_none() { + return Err(InvalidBindDn { + dn: input.to_string(), + kind: InvalidBindDnKind::NoDc, + }); + } + + Ok(Self(dn)) + } + + pub fn to_dn_string(&self) -> String { + self.0.to_dn_string() + } + + pub fn to_user_ref(&self) -> UserRef { + let username = self.0.keys.get("uid").unwrap()[0].clone(); + let domain = self.0.keys.get("dc").unwrap().join("."); + + UserRef { username, domain } + } +} #[derive(Debug)] pub enum BindError { Db(BoxedError), InvalidCredentials, - InvalidDn(InvalidDnError), - NotUserDn(NotUserDnError), + InvalidDn(InvalidBindDn), UnsupportedSASL, } @@ -42,19 +116,12 @@ impl BindError { } } -impl From for BindError { - fn from(e: InvalidDnError) -> Self { - Self::InvalidDn(e) - } -} - impl LdapReturnError for BindError { fn code(&self) -> LdapResultCode { match self { Self::Db(_e) => LdapResultCode::Unavailable, Self::InvalidCredentials => LdapResultCode::InvalidCredentials, Self::InvalidDn(e) => e.code(), - Self::NotUserDn(e) => e.code(), Self::UnsupportedSASL => LdapResultCode::OperationsError, } } @@ -64,7 +131,6 @@ impl LdapReturnError for BindError { Self::Db(e) => format!("Database error: {e}"), Self::InvalidCredentials => "Wrong username or password".to_string(), Self::InvalidDn(e) => e.message(), - Self::NotUserDn(e) => e.message(), Self::UnsupportedSASL => "SASL login is not supported".to_string(), } } @@ -98,8 +164,14 @@ pub async fn op_bind( db: &Database, req: LdapBindRequest, msgid: i32, -) -> Result, LdapStreamError> { - let dn = match Dn::from_dn_str(&req.dn) { +) -> Result, LdapStreamError> { + // Anonymous bind + if req.dn.is_empty() { + bind_success(stream, msgid).await?; + return Ok(None); + } + + let dn = match BindDn::from_dn_str(&req.dn) { Ok(dn) => dn, Err(e) => { BindError::InvalidDn(e).error_message(stream, msgid).await?; @@ -114,15 +186,7 @@ pub async fn op_bind( return Ok(None); }; - let user_ref = match dn.to_user_ref() { - Ok(user_ref) => user_ref, - Err(e) => { - BindError::NotUserDn(e).error_message(stream, msgid).await?; - return Ok(None); - } - }; - - let success = match db.check_password(&user_ref, &password).await { + let success = match db.check_password(&dn.to_user_ref(), &password).await { Ok(success) => success, Err(e) => { // tracing::error!(error = &*e as &dyn std::error::Error, "Database failure"); diff --git a/src/ldap/op/mod.rs b/src/ldap/op/mod.rs index 07acdea..8febca5 100644 --- a/src/ldap/op/mod.rs +++ b/src/ldap/op/mod.rs @@ -1,2 +1,3 @@ pub mod bind; pub mod ext; +pub mod search; diff --git a/src/ldap/op/search.rs b/src/ldap/op/search.rs new file mode 100644 index 0000000..3255298 --- /dev/null +++ b/src/ldap/op/search.rs @@ -0,0 +1,217 @@ +use ldap3_proto::control::LdapControl; +use ldap3_proto::proto::{ + LdapOp, LdapPartialAttribute, LdapResult, LdapSearchRequest, LdapSearchResultEntry, +}; +use ldap3_proto::{LdapMsg, LdapResultCode}; + +use crate::db::error::BoxedError; +use crate::db::{Database, DatabaseInterface, User}; +use crate::ldap::filter::mail::{MailDomainError, MailFilter}; +use crate::ldap::{Dn, LdapReturnError, LdapStream, LdapStreamError, MalformedDn}; + +#[derive(Debug)] +pub struct InvalidSearchDn { + dn: String, + kind: InvalidSearchDnKind, +} + +#[derive(Debug)] +pub enum InvalidSearchDnKind { + Malformed(MalformedDn), + NoOu, + MultipleOu, +} + +impl LdapReturnError for InvalidSearchDn { + fn code(&self) -> LdapResultCode { + LdapResultCode::InvalidDNSyntax + } + + fn message(&self) -> String { + match &self.kind { + InvalidSearchDnKind::Malformed(_e) => format!("Malformed dn: {}", self.dn), + InvalidSearchDnKind::NoOu => format!("Missing `ou` in search dn: {}", self.dn), + InvalidSearchDnKind::MultipleOu => { + format!("Multiple `ou` tables provided in search dn: {}", self.dn) + } + } + } +} + +#[derive(Clone, Debug)] +pub struct SearchDn(Dn); + +impl SearchDn { + pub fn from_dn_str(input: &str) -> Result { + let dn = Dn::from_dn_str(input).map_err(|e| InvalidSearchDn { + dn: input.to_string(), + kind: InvalidSearchDnKind::Malformed(e), + })?; + + let Some(ou) = dn.keys.get("ou") else { + return Err(InvalidSearchDn { + dn: input.to_string(), + kind: InvalidSearchDnKind::NoOu, + }); + }; + + if ou.len() != 1 { + return Err(InvalidSearchDn { + dn: input.to_string(), + kind: InvalidSearchDnKind::MultipleOu, + }); + } + + Ok(Self(dn)) + } +} + +#[derive(Debug)] +pub enum SearchError { + Db(BoxedError), + InvalidDn(InvalidSearchDn), + MailDomain(MailDomainError), +} + +impl SearchError { + pub async fn error_message( + &self, + stream: &mut LdapStream, + msgid: i32, + ) -> Result<(), LdapStreamError> { + let resp_msg = LdapMsg { + msgid, + op: LdapOp::SearchResultDone(LdapResult { + code: self.code(), + matcheddn: String::new(), + message: self.message(), + referral: vec![], + }), + ctrl: vec![], + }; + + stream.send(resp_msg).await?; + Ok(()) + } +} + +impl LdapReturnError for SearchError { + fn code(&self) -> LdapResultCode { + match self { + Self::Db(_e) => LdapResultCode::Unavailable, + Self::InvalidDn(e) => e.code(), + Self::MailDomain(e) => e.code(), + } + } + + fn message(&self) -> String { + match self { + Self::Db(e) => format!("Database error: {e}"), + Self::InvalidDn(e) => e.message(), + Self::MailDomain(e) => e.message(), + } + } +} + +pub async fn search_success( + stream: &mut LdapStream, + msgid: i32, + entries: Vec<(LdapSearchResultEntry, Vec)>, +) -> Result<(), LdapStreamError> { + let count = entries.len(); + + for (entry, ctrl) in entries { + tracing::debug!("Search result: {entry:?}"); + stream + .send(LdapMsg { + msgid, + op: LdapOp::SearchResultEntry(entry), + ctrl, + }) + .await?; + } + + stream + .send(LdapMsg { + msgid, + op: LdapOp::SearchResultDone(LdapResult { + code: LdapResultCode::Success, + matcheddn: String::new(), + message: format!("Found {count} result(s)"), + referral: vec![], + }), + // TODO: implement LdapControl for pagination + ctrl: vec![], + }) + .await?; + + Ok(()) +} + +fn search_entry_from_user(user: &User, req_attrs: &[String]) -> LdapSearchResultEntry { + let mut res: Vec = vec![]; + for attr in req_attrs { + if let Some(attr_value) = match attr.as_str() { + "uid" => Some(user.username.clone()), + "cn"|"mail" => Some(user.mail.clone()), + _ => { + tracing::warn!("Ignoring unknown attr in search query: {attr}"); + None + } + } { + res.push(LdapPartialAttribute { + atype: attr.clone(), + // TODO: there may be multiple values here in the future, + // eg. mailaliases + vals: vec![Vec::from(attr_value)], + }); + } + } + + LdapSearchResultEntry { + dn: Dn::from_user(user).to_dn_string(), + attributes: res, + } +} + +pub async fn search_by_mail_filter( + stream: &mut LdapStream, + db: &Database, + sr: LdapSearchRequest, + msgid: i32, +) -> Result<(), LdapStreamError> { + // TODO: We should probably reuse the search DN somehow + if let Err(e) = SearchDn::from_dn_str(&sr.base) { + SearchError::InvalidDn(e) + .error_message(stream, msgid) + .await?; + return Ok(()); + } + + let mail_filter = match MailFilter::from_search_filter(&sr.filter) { + Ok(mail_filter) => mail_filter, + Err(e) => { + SearchError::MailDomain(e) + .error_message(stream, msgid) + .await?; + return Ok(()); + } + }; + + let maybe_user = match db.get_user(&mail_filter.to_user_ref()).await { + Ok(maybe_user) => maybe_user, + Err(e) => { + SearchError::Db(e).error_message(stream, msgid).await?; + return Ok(()); + } + }; + + if let Some(user) = maybe_user { + let entry_ctrl = (search_entry_from_user(&user, &sr.attrs), vec![]); + search_success(stream, msgid, vec![entry_ctrl]).await?; + } else { + search_success(stream, msgid, vec![]).await?; + } + + Ok(()) +}