Improved error handling (part 1)

This commit is contained in:
mdecimus
2024-07-11 18:44:51 +02:00
parent ea77a98260
commit 0c2a3f09fe
179 changed files with 3409 additions and 3048 deletions

View File

@@ -7,14 +7,18 @@
use mail_send::Credentials;
use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
use crate::{DirectoryError, Principal, QueryBy};
use crate::{IntoError, Principal, QueryBy};
use super::{ImapDirectory, ImapError};
impl ImapDirectory {
pub async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
if let QueryBy::Credentials(credentials) = query {
let mut client = self.pool.get().await?;
let mut client = self
.pool
.get()
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
let mechanism = match credentials {
Credentials::Plain { .. }
if (client.mechanisms & (AUTH_PLAIN | AUTH_LOGIN | AUTH_CRAM_MD5)) != 0 =>
@@ -34,13 +38,12 @@ impl ImapDirectory {
AUTH_XOAUTH2
}
_ => {
tracing::warn!(
context = "remote",
event = "error",
protocol = "imap",
"IMAP server does not offer any supported auth mechanisms.",
);
return Ok(None);
trc::bail!(trc::Cause::Unsupported
.ctx(
trc::Key::Reason,
"IMAP server does not offer any supported auth mechanisms."
)
.protocol(trc::Protocol::Imap));
}
};
@@ -51,31 +54,41 @@ impl ImapDirectory {
}
Err(err) => match &err {
ImapError::AuthenticationFailed => Ok(None),
_ => Err(err.into()),
_ => Err(err.into_error()),
},
}
} else {
Err(DirectoryError::unsupported("imap", "query"))
Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
}
pub async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
Err(DirectoryError::unsupported("imap", "email_to_ids"))
pub async fn email_to_ids(&self, _address: &str) -> trc::Result<Vec<u32>> {
Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
pub async fn rcpt(&self, _address: &str) -> crate::Result<bool> {
Err(DirectoryError::unsupported("imap", "rcpt"))
pub async fn rcpt(&self, _address: &str) -> trc::Result<bool> {
Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
pub async fn vrfy(&self, _address: &str) -> crate::Result<Vec<String>> {
Err(DirectoryError::unsupported("imap", "vrfy"))
pub async fn vrfy(&self, _address: &str) -> trc::Result<Vec<String>> {
Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
pub async fn expn(&self, _address: &str) -> crate::Result<Vec<String>> {
Err(DirectoryError::unsupported("imap", "expn"))
pub async fn expn(&self, _address: &str) -> trc::Result<Vec<String>> {
Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
Ok(self.domains.contains(domain))
}
}

View File

@@ -20,13 +20,13 @@ pub trait DirectoryStore: Sync + Send {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>>;
async fn email_to_ids(&self, email: &str) -> crate::Result<Vec<u32>>;
) -> trc::Result<Option<Principal<u32>>>;
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>>;
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool>;
async fn rcpt(&self, address: &str) -> crate::Result<bool>;
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>>;
async fn expn(&self, address: &str) -> crate::Result<Vec<String>>;
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>;
async fn rcpt(&self, address: &str) -> trc::Result<bool>;
async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>>;
async fn expn(&self, address: &str) -> trc::Result<Vec<String>>;
}
impl DirectoryStore for Store {
@@ -34,7 +34,7 @@ impl DirectoryStore for Store {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal<u32>>> {
let (account_id, secret) = match by {
QueryBy::Name(name) => (self.get_account_id(name).await?, None),
QueryBy::Id(account_id) => (account_id.into(), None),
@@ -79,7 +79,7 @@ impl DirectoryStore for Store {
}
}
async fn email_to_ids(&self, email: &str) -> crate::Result<Vec<u32>> {
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>> {
if let Some(ptype) = self
.get_value::<PrincipalIdType>(ValueKey::from(ValueClass::Directory(
DirectoryClass::EmailToId(email.as_bytes().to_vec()),
@@ -89,32 +89,30 @@ impl DirectoryStore for Store {
if ptype.typ != Type::List {
Ok(vec![ptype.account_id])
} else {
self.get_members(ptype.account_id).await.map_err(Into::into)
self.get_members(ptype.account_id).await
}
} else {
Ok(Vec::new())
}
}
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
self.get_value::<()>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Domain(domain.as_bytes().to_vec()),
)))
.await
.map(|ids| ids.is_some())
.map_err(Into::into)
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
async fn rcpt(&self, address: &str) -> trc::Result<bool> {
self.get_value::<()>(ValueKey::from(ValueClass::Directory(
DirectoryClass::EmailToId(address.as_bytes().to_vec()),
)))
.await
.map(|ids| ids.is_some())
.map_err(Into::into)
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
let mut results = Vec::new();
let address = address.split('@').next().unwrap_or(address);
if address.len() > 3 {
@@ -141,7 +139,7 @@ impl DirectoryStore for Store {
Ok(results)
}
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
let mut results = Vec::new();
for account_id in self.email_to_ids(address).await? {
if let Some(email) = self

View File

@@ -12,8 +12,9 @@ use store::{
},
Deserialize, IterateParams, Serialize, Store, ValueKey, U32_LEN,
};
use trc::AddContext;
use crate::{DirectoryError, ManagementError, Principal, QueryBy, Type};
use crate::{Principal, QueryBy, Type};
use super::{
lookup::DirectoryStore, PrincipalAction, PrincipalField, PrincipalIdType, PrincipalUpdate,
@@ -22,83 +23,74 @@ use super::{
#[allow(async_fn_in_trait)]
pub trait ManageDirectory: Sized {
async fn get_account_id(&self, name: &str) -> crate::Result<Option<u32>>;
async fn get_or_create_account_id(&self, name: &str) -> crate::Result<u32>;
async fn get_account_name(&self, account_id: u32) -> crate::Result<Option<String>>;
async fn get_member_of(&self, account_id: u32) -> crate::Result<Vec<u32>>;
async fn get_members(&self, account_id: u32) -> crate::Result<Vec<u32>>;
async fn get_account_id(&self, name: &str) -> trc::Result<Option<u32>>;
async fn get_or_create_account_id(&self, name: &str) -> trc::Result<u32>;
async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>>;
async fn get_member_of(&self, account_id: u32) -> trc::Result<Vec<u32>>;
async fn get_members(&self, account_id: u32) -> trc::Result<Vec<u32>>;
async fn create_account(
&self,
principal: Principal<String>,
members: Vec<String>,
) -> crate::Result<u32>;
) -> trc::Result<u32>;
async fn update_account(
&self,
by: QueryBy<'_>,
changes: Vec<PrincipalUpdate>,
) -> crate::Result<()>;
async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()>;
) -> trc::Result<()>;
async fn delete_account(&self, by: QueryBy<'_>) -> trc::Result<()>;
async fn list_accounts(
&self,
filter: Option<&str>,
typ: Option<Type>,
) -> crate::Result<Vec<String>>;
async fn map_group_ids(&self, principal: Principal<u32>) -> crate::Result<Principal<String>>;
) -> trc::Result<Vec<String>>;
async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>>;
async fn map_principal(
&self,
principal: Principal<String>,
create_if_missing: bool,
) -> crate::Result<Principal<u32>>;
) -> trc::Result<Principal<u32>>;
async fn map_group_names(
&self,
members: Vec<String>,
create_if_missing: bool,
) -> crate::Result<Vec<u32>>;
async fn create_domain(&self, domain: &str) -> crate::Result<()>;
async fn delete_domain(&self, domain: &str) -> crate::Result<()>;
async fn list_domains(&self, filter: Option<&str>) -> crate::Result<Vec<String>>;
) -> trc::Result<Vec<u32>>;
async fn create_domain(&self, domain: &str) -> trc::Result<()>;
async fn delete_domain(&self, domain: &str) -> trc::Result<()>;
async fn list_domains(&self, filter: Option<&str>) -> trc::Result<Vec<String>>;
}
impl ManageDirectory for Store {
async fn get_account_name(&self, account_id: u32) -> crate::Result<Option<String>> {
async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>> {
self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
.map_err(Into::into)
.map(|v| {
if let Some(v) = v {
Some(v.name)
} else {
tracing::debug!(
context = "directory",
event = "not_found",
account = account_id,
"Principal not found for account id"
);
None
}
})
.map(|v| if let Some(v) = v { Some(v.name) } else { None })
.caused_by(trc::location!())
}
async fn get_account_id(&self, name: &str) -> crate::Result<Option<u32>> {
async fn get_account_id(&self, name: &str) -> trc::Result<Option<u32>> {
self.get_value::<PrincipalIdType>(ValueKey::from(ValueClass::Directory(
DirectoryClass::NameToId(name.as_bytes().to_vec()),
)))
.await
.map(|v| v.map(|v| v.account_id))
.map_err(Into::into)
.caused_by(trc::location!())
}
// Used by all directories except internal
async fn get_or_create_account_id(&self, name: &str) -> crate::Result<u32> {
async fn get_or_create_account_id(&self, name: &str) -> trc::Result<u32> {
let mut try_count = 0;
let name = name.to_lowercase();
loop {
// Try to obtain ID
if let Some(account_id) = self.get_account_id(&name).await? {
if let Some(account_id) = self
.get_account_id(&name)
.await
.caused_by(trc::location!())?
{
return Ok(account_id);
}
@@ -129,16 +121,13 @@ impl ManageDirectory for Store {
Ok(account_id) => {
return Ok(account_id);
}
Err(store::Error::AssertValueFailed) if try_count < 3 => {
try_count += 1;
continue;
}
Err(err) => {
tracing::error!(event = "error",
context = "store",
error = ?err,
"Failed to generate account id");
return Err(err.into());
if err.matches(trc::Cause::AssertValue) && try_count < 3 {
try_count += 1;
continue;
} else {
return Err(err.caused_by(trc::location!()));
}
}
}
}
@@ -148,41 +137,46 @@ impl ManageDirectory for Store {
&self,
principal: Principal<String>,
members: Vec<String>,
) -> crate::Result<u32> {
) -> trc::Result<u32> {
// Make sure the principal has a name
if principal.name.is_empty() {
return Err(DirectoryError::Management(ManagementError::MissingField(
PrincipalField::Name,
)));
return Err(not_found(PrincipalField::Name));
}
// Map group names
let mut principal = self.map_principal(principal, false).await?;
let members = self.map_group_names(members, false).await?;
let mut principal = self
.map_principal(principal, false)
.await
.caused_by(trc::location!())?;
let members = self
.map_group_names(members, false)
.await
.caused_by(trc::location!())?;
// Make sure new name is not taken
principal.name = principal.name.to_lowercase();
if self.get_account_id(&principal.name).await?.is_some() {
return Err(DirectoryError::Management(ManagementError::AlreadyExists {
field: PrincipalField::Name,
value: principal.name,
}));
if self
.get_account_id(&principal.name)
.await
.caused_by(trc::location!())?
.is_some()
{
return Err(err_exists(PrincipalField::Name, principal.name));
}
// Make sure the e-mail is not taken and validate domain
for email in principal.emails.iter_mut() {
*email = email.to_lowercase();
if self.rcpt(email).await? {
return Err(DirectoryError::Management(ManagementError::AlreadyExists {
field: PrincipalField::Emails,
value: email.to_string(),
}));
if self.rcpt(email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email.to_string()));
}
if let Some(domain) = email.split('@').nth(1) {
if !self.is_local_domain(domain).await? {
return Err(DirectoryError::Management(ManagementError::NotFound(
domain.to_string(),
)));
if !self
.is_local_domain(domain)
.await
.caused_by(trc::location!())?
{
return Err(not_found(domain.to_string()));
}
}
}
@@ -254,14 +248,15 @@ impl ManageDirectory for Store {
self.write(batch.build())
.await
.and_then(|r| r.last_document_id())
.map_err(Into::into)
}
async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()> {
async fn delete_account(&self, by: QueryBy<'_>) -> trc::Result<()> {
let account_id = match by {
QueryBy::Name(name) => self.get_account_id(name).await?.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(name.to_string()))
})?,
QueryBy::Name(name) => self
.get_account_id(name)
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(name.to_string()))?,
QueryBy::Id(account_id) => account_id,
QueryBy::Credentials(_) => unreachable!(),
};
@@ -270,19 +265,24 @@ impl ManageDirectory for Store {
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await?
.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(account_id.to_string()))
})?;
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(account_id.to_string()))?;
// Unlink all account's blobs
self.blob_hash_unlink_account(account_id).await?;
self.blob_hash_unlink_account(account_id)
.await
.caused_by(trc::location!())?;
// Revoke ACLs
self.acl_revoke_all(account_id).await?;
self.acl_revoke_all(account_id)
.await
.caused_by(trc::location!())?;
// Delete account data
self.purge_account(account_id).await?;
self.purge_account(account_id)
.await
.caused_by(trc::location!())?;
// Delete account
let mut batch = BatchBuilder::new();
@@ -298,7 +298,11 @@ impl ManageDirectory for Store {
batch.clear(DirectoryClass::EmailToId(email.into_bytes()));
}
for member_id in self.get_member_of(account_id).await? {
for member_id in self
.get_member_of(account_id)
.await
.caused_by(trc::location!())?
{
batch.clear(DirectoryClass::MemberOf {
principal_id: MaybeDynamicId::Static(account_id),
member_of: MaybeDynamicId::Static(member_id),
@@ -309,7 +313,11 @@ impl ManageDirectory for Store {
});
}
for member_id in self.get_members(account_id).await? {
for member_id in self
.get_members(account_id)
.await
.caused_by(trc::location!())?
{
batch.clear(DirectoryClass::MemberOf {
principal_id: MaybeDynamicId::Static(member_id),
member_of: MaybeDynamicId::Static(account_id),
@@ -320,7 +328,9 @@ impl ManageDirectory for Store {
});
}
self.write(batch.build()).await?;
self.write(batch.build())
.await
.caused_by(trc::location!())?;
Ok(())
}
@@ -329,11 +339,13 @@ impl ManageDirectory for Store {
&self,
by: QueryBy<'_>,
changes: Vec<PrincipalUpdate>,
) -> crate::Result<()> {
) -> trc::Result<()> {
let account_id = match by {
QueryBy::Name(name) => self.get_account_id(name).await?.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(name.to_string()))
})?,
QueryBy::Name(name) => self
.get_account_id(name)
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(name.to_string()))?,
QueryBy::Id(account_id) => account_id,
QueryBy::Credentials(_) => unreachable!(),
};
@@ -343,14 +355,19 @@ impl ManageDirectory for Store {
.get_value::<HashedValue<Principal<u32>>>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await?
.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(account_id.to_string()))
})?;
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(account_id.to_string()))?;
// Obtain members and memberOf
let mut member_of = self.get_member_of(account_id).await?;
let mut members = self.get_members(account_id).await?;
let mut member_of = self
.get_member_of(account_id)
.await
.caused_by(trc::location!())?;
let mut members = self
.get_members(account_id)
.await
.caused_by(trc::location!())?;
// Apply changes
let mut batch = BatchBuilder::new();
@@ -375,13 +392,13 @@ impl ManageDirectory for Store {
// Make sure new name is not taken
let new_name = new_name.to_lowercase();
if principal.inner.name != new_name {
if self.get_account_id(&new_name).await?.is_some() {
return Err(DirectoryError::Management(
ManagementError::AlreadyExists {
field: PrincipalField::Name,
value: new_name,
},
));
if self
.get_account_id(&new_name)
.await
.caused_by(trc::location!())?
.is_some()
{
return Err(err_exists(PrincipalField::Name, new_name));
}
batch.clear(ValueClass::Directory(DirectoryClass::NameToId(
@@ -405,7 +422,7 @@ impl ManageDirectory for Store {
continue;
}
}
return Err(DirectoryError::Unsupported);
return Err(trc::Cause::Unsupported.caused_by(trc::location!()));
}
(
PrincipalAction::Set,
@@ -472,19 +489,16 @@ impl ManageDirectory for Store {
.collect::<Vec<_>>();
for email in &emails {
if !principal.inner.emails.contains(email) {
if self.rcpt(email).await? {
return Err(DirectoryError::Management(
ManagementError::AlreadyExists {
field: PrincipalField::Emails,
value: email.to_string(),
},
));
if self.rcpt(email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email.to_string()));
}
if let Some(domain) = email.split('@').nth(1) {
if !self.is_local_domain(domain).await? {
return Err(DirectoryError::Management(
ManagementError::NotFound(domain.to_string()),
));
if !self
.is_local_domain(domain)
.await
.caused_by(trc::location!())?
{
return Err(not_found(domain.to_string()));
}
}
batch.set(
@@ -513,19 +527,16 @@ impl ManageDirectory for Store {
) => {
let email = email.to_lowercase();
if !principal.inner.emails.contains(&email) {
if self.rcpt(&email).await? {
return Err(DirectoryError::Management(
ManagementError::AlreadyExists {
field: PrincipalField::Emails,
value: email,
},
));
if self.rcpt(&email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email));
}
if let Some(domain) = email.split('@').nth(1) {
if !self.is_local_domain(domain).await? {
return Err(DirectoryError::Management(ManagementError::NotFound(
domain.to_string(),
)));
if !self
.is_local_domain(domain)
.await
.caused_by(trc::location!())?
{
return Err(not_found(domain.to_string()));
}
}
batch.set(
@@ -559,9 +570,11 @@ impl ManageDirectory for Store {
) => {
let mut new_member_of = Vec::new();
for member in members {
let member_id = self.get_account_id(&member).await?.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(member))
})?;
let member_id = self
.get_account_id(&member)
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(member))?;
if !member_of.contains(&member_id) {
batch.set(
ValueClass::Directory(DirectoryClass::MemberOf {
@@ -602,9 +615,11 @@ impl ManageDirectory for Store {
PrincipalField::MemberOf,
PrincipalValue::String(member),
) => {
let member_id = self.get_account_id(&member).await?.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(member))
})?;
let member_id = self
.get_account_id(&member)
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(member))?;
if !member_of.contains(&member_id) {
batch.set(
ValueClass::Directory(DirectoryClass::MemberOf {
@@ -628,7 +643,11 @@ impl ManageDirectory for Store {
PrincipalField::MemberOf,
PrincipalValue::String(member),
) => {
if let Some(member_id) = self.get_account_id(&member).await? {
if let Some(member_id) = self
.get_account_id(&member)
.await
.caused_by(trc::location!())?
{
if let Some(pos) = member_of.iter().position(|v| *v == member_id) {
batch.clear(ValueClass::Directory(DirectoryClass::MemberOf {
principal_id: MaybeDynamicId::Static(account_id),
@@ -650,9 +669,11 @@ impl ManageDirectory for Store {
) => {
let mut new_members = Vec::new();
for member in members_ {
let member_id = self.get_account_id(&member).await?.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(member))
})?;
let member_id = self
.get_account_id(&member)
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(member))?;
if !members.contains(&member_id) {
batch.set(
ValueClass::Directory(DirectoryClass::MemberOf {
@@ -693,9 +714,11 @@ impl ManageDirectory for Store {
PrincipalField::Members,
PrincipalValue::String(member),
) => {
let member_id = self.get_account_id(&member).await?.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(member))
})?;
let member_id = self
.get_account_id(&member)
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(member))?;
if !members.contains(&member_id) {
batch.set(
ValueClass::Directory(DirectoryClass::MemberOf {
@@ -719,7 +742,11 @@ impl ManageDirectory for Store {
PrincipalField::Members,
PrincipalValue::String(member),
) => {
if let Some(member_id) = self.get_account_id(&member).await? {
if let Some(member_id) = self
.get_account_id(&member)
.await
.caused_by(trc::location!())?
{
if let Some(pos) = members.iter().position(|v| *v == member_id) {
batch.clear(ValueClass::Directory(DirectoryClass::MemberOf {
principal_id: MaybeDynamicId::Static(member_id),
@@ -735,7 +762,7 @@ impl ManageDirectory for Store {
}
_ => {
return Err(DirectoryError::Unsupported);
return Err(trc::Cause::Unsupported.caused_by(trc::location!()));
}
}
}
@@ -749,45 +776,37 @@ impl ManageDirectory for Store {
);
}
self.write(batch.build()).await?;
self.write(batch.build())
.await
.caused_by(trc::location!())?;
Ok(())
}
async fn create_domain(&self, domain: &str) -> crate::Result<()> {
async fn create_domain(&self, domain: &str) -> trc::Result<()> {
if !domain.contains('.') {
return Err(DirectoryError::Management(ManagementError::MissingField(
PrincipalField::Name,
)));
return Err(err_missing(PrincipalField::Name));
}
let mut batch = BatchBuilder::new();
batch.set(
ValueClass::Directory(DirectoryClass::Domain(domain.to_lowercase().into_bytes())),
vec![],
);
self.write(batch.build())
.await
.map_err(Into::into)
.map(|_| ())
self.write(batch.build()).await.map(|_| ())
}
async fn delete_domain(&self, domain: &str) -> crate::Result<()> {
async fn delete_domain(&self, domain: &str) -> trc::Result<()> {
if !domain.contains('.') {
return Err(DirectoryError::Management(ManagementError::MissingField(
PrincipalField::Name,
)));
return Err(err_missing(PrincipalField::Name));
}
let mut batch = BatchBuilder::new();
batch.clear(ValueClass::Directory(DirectoryClass::Domain(
domain.to_lowercase().into_bytes(),
)));
self.write(batch.build())
.await
.map_err(Into::into)
.map(|_| ())
self.write(batch.build()).await.map(|_| ())
}
async fn map_group_ids(&self, principal: Principal<u32>) -> crate::Result<Principal<String>> {
async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>> {
let mut mapped = Principal {
id: principal.id,
typ: principal.typ,
@@ -800,7 +819,11 @@ impl ManageDirectory for Store {
};
for account_id in principal.member_of {
if let Some(name) = self.get_account_name(account_id).await? {
if let Some(name) = self
.get_account_name(account_id)
.await
.caused_by(trc::location!())?
{
mapped.member_of.push(name);
}
}
@@ -812,7 +835,7 @@ impl ManageDirectory for Store {
&self,
principal: Principal<String>,
create_if_missing: bool,
) -> crate::Result<Principal<u32>> {
) -> trc::Result<Principal<u32>> {
Ok(Principal {
id: principal.id,
typ: principal.typ,
@@ -822,7 +845,8 @@ impl ManageDirectory for Store {
emails: principal.emails,
member_of: self
.map_group_names(principal.member_of, create_if_missing)
.await?,
.await
.caused_by(trc::location!())?,
description: principal.description,
})
}
@@ -831,16 +855,19 @@ impl ManageDirectory for Store {
&self,
members: Vec<String>,
create_if_missing: bool,
) -> crate::Result<Vec<u32>> {
) -> trc::Result<Vec<u32>> {
let mut member_ids = Vec::with_capacity(members.len());
for member in members {
let account_id = if create_if_missing {
self.get_or_create_account_id(&member).await?
self.get_or_create_account_id(&member)
.await
.caused_by(trc::location!())?
} else {
self.get_account_id(&member)
.await?
.ok_or_else(|| DirectoryError::Management(ManagementError::NotFound(member)))?
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(member))?
};
member_ids.push(account_id);
}
@@ -852,7 +879,7 @@ impl ManageDirectory for Store {
&self,
filter: Option<&str>,
typ: Option<Type>,
) -> crate::Result<Vec<String>> {
) -> trc::Result<Vec<String>> {
let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![])));
let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![
u8::MAX;
@@ -863,7 +890,7 @@ impl ManageDirectory for Store {
self.iterate(
IterateParams::new(from_key, to_key).ascending(),
|key, value| {
let pt = PrincipalIdType::deserialize(value)?;
let pt = PrincipalIdType::deserialize(value).caused_by(trc::location!())?;
if typ.map_or(true, |t| pt.typ == t) {
results.push((
@@ -875,7 +902,8 @@ impl ManageDirectory for Store {
Ok(true)
},
)
.await?;
.await
.caused_by(trc::location!())?;
if let Some(filter) = filter {
let mut filtered = Vec::new();
@@ -889,12 +917,9 @@ impl ManageDirectory for Store {
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await?
.ok_or_else(|| {
DirectoryError::Management(ManagementError::NotFound(
account_id.to_string(),
))
})?;
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(account_id.to_string()))?;
if filters.iter().all(|f| {
principal.name.to_lowercase().contains(f)
|| principal
@@ -916,7 +941,7 @@ impl ManageDirectory for Store {
}
}
async fn list_domains(&self, filter: Option<&str>) -> crate::Result<Vec<String>> {
async fn list_domains(&self, filter: Option<&str>) -> trc::Result<Vec<String>> {
let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![])));
let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Domain(vec![
u8::MAX;
@@ -934,12 +959,13 @@ impl ManageDirectory for Store {
Ok(true)
},
)
.await?;
.await
.caused_by(trc::location!())?;
Ok(results)
}
async fn get_member_of(&self, account_id: u32) -> crate::Result<Vec<u32>> {
async fn get_member_of(&self, account_id: u32) -> trc::Result<Vec<u32>> {
let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::MemberOf {
principal_id: account_id,
member_of: 0,
@@ -956,11 +982,12 @@ impl ManageDirectory for Store {
Ok(true)
},
)
.await?;
.await
.caused_by(trc::location!())?;
Ok(results)
}
async fn get_members(&self, account_id: u32) -> crate::Result<Vec<u32>> {
async fn get_members(&self, account_id: u32) -> trc::Result<Vec<u32>> {
let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::Members {
principal_id: account_id,
has_member: 0,
@@ -977,15 +1004,16 @@ impl ManageDirectory for Store {
Ok(true)
},
)
.await?;
.await
.caused_by(trc::location!())?;
Ok(results)
}
}
impl SerializeWithId for Principal<u32> {
fn serialize_with_id(&self, ids: &AssignedIds) -> store::Result<Vec<u8>> {
fn serialize_with_id(&self, ids: &AssignedIds) -> trc::Result<Vec<u8>> {
let mut principal = self.clone();
principal.id = ids.last_document_id()?;
principal.id = ids.last_document_id().caused_by(trc::location!())?;
Ok(principal.serialize())
}
}
@@ -1000,7 +1028,7 @@ impl From<Principal<u32>> for MaybeDynamicValue {
struct DynamicPrincipalIdType(Type);
impl SerializeWithId for DynamicPrincipalIdType {
fn serialize_with_id(&self, ids: &AssignedIds) -> store::Result<Vec<u8>> {
fn serialize_with_id(&self, ids: &AssignedIds) -> trc::Result<Vec<u8>> {
ids.last_document_id()
.map(|account_id| PrincipalIdType::new(account_id, self.0).serialize())
}
@@ -1026,3 +1054,23 @@ impl From<Principal<String>> for Principal<u32> {
}
}
}
fn err_missing(field: impl Into<trc::Value>) -> trc::Error {
trc::Cause::MissingParameter.ctx(trc::Key::Key, field)
}
fn err_exists(field: impl Into<trc::Value>, value: impl Into<trc::Value>) -> trc::Error {
trc::Cause::AlreadyExists
.ctx(trc::Key::Key, field)
.ctx(trc::Key::Value, value)
}
fn not_found(value: impl Into<trc::Value>) -> trc::Error {
trc::Cause::NotFound.ctx(trc::Key::Key, value)
}
impl From<PrincipalField> for trc::Value {
fn from(value: PrincipalField) -> Self {
trc::Value::Static(value.as_str())
}
}

View File

@@ -56,9 +56,12 @@ impl Serialize for &Principal<u32> {
}
impl Deserialize for Principal<u32> {
fn deserialize(bytes: &[u8]) -> store::Result<Self> {
deserialize(bytes)
.ok_or_else(|| store::Error::InternalError("Failed to deserialize principal".into()))
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
deserialize(bytes).ok_or_else(|| {
trc::Cause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes)
})
}
}
@@ -72,14 +75,18 @@ impl Serialize for PrincipalIdType {
}
impl Deserialize for PrincipalIdType {
fn deserialize(bytes: &[u8]) -> store::Result<Self> {
let mut bytes = bytes.iter();
fn deserialize(bytes_: &[u8]) -> trc::Result<Self> {
let mut bytes = bytes_.iter();
Ok(PrincipalIdType {
account_id: bytes.next_leb128().ok_or_else(|| {
store::Error::InternalError("Failed to deserialize principal account id".into())
trc::Cause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes_)
})?,
typ: Type::from_u8(*bytes.next().ok_or_else(|| {
store::Error::InternalError("Failed to deserialize principal id type".into())
trc::Cause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes_)
})?),
})
}
@@ -189,15 +196,21 @@ impl PrincipalUpdate {
impl Display for PrincipalField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
impl PrincipalField {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalField::Name => write!(f, "name"),
PrincipalField::Type => write!(f, "type"),
PrincipalField::Quota => write!(f, "quota"),
PrincipalField::Description => write!(f, "description"),
PrincipalField::Secrets => write!(f, "secrets"),
PrincipalField::Emails => write!(f, "emails"),
PrincipalField::MemberOf => write!(f, "memberOf"),
PrincipalField::Members => write!(f, "members"),
PrincipalField::Name => "name",
PrincipalField::Type => "type",
PrincipalField::Quota => "quota",
PrincipalField::Description => "description",
PrincipalField::Secrets => "secrets",
PrincipalField::Emails => "emails",
PrincipalField::MemberOf => "memberOf",
PrincipalField::Members => "members",
}
}
}

View File

@@ -4,10 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use ldap3::{Ldap, LdapConnAsync, LdapError, Scope, SearchEntry};
use ldap3::{Ldap, LdapConnAsync, Scope, SearchEntry};
use mail_send::Credentials;
use crate::{backend::internal::manage::ManageDirectory, DirectoryError, Principal, QueryBy, Type};
use crate::{backend::internal::manage::ManageDirectory, IntoError, Principal, QueryBy, Type};
use super::{LdapDirectory, LdapMappings};
@@ -16,8 +16,8 @@ impl LdapDirectory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> {
let mut conn = self.pool.get().await?;
) -> trc::Result<Option<Principal<u32>>> {
let mut conn = self.pool.get().await.map_err(|err| err.into_error())?;
let mut account_id = None;
let account_name;
@@ -64,19 +64,26 @@ impl LdapDirectory {
self.pool.manager().settings.clone(),
&self.pool.manager().address,
)
.await?;
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
ldap3::drive!(conn);
ldap.simple_bind(&auth_bind.build(username), secret).await?;
ldap.simple_bind(&auth_bind.build(username), secret)
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
match self
.find_principal(&mut ldap, &self.mappings.filter_name.build(username))
.await
{
Ok(Some(principal)) => principal,
Err(DirectoryError::Ldap(LdapError::LdapResult { result }))
if [49, 50].contains(&result.rc) =>
Err(err)
if err.matches(trc::Cause::Ldap)
&& err
.value(trc::Key::Code)
.and_then(|v| v.to_uint())
.map_or(false, |rc| [49, 50].contains(&rc)) =>
{
return Ok(None);
}
@@ -90,13 +97,6 @@ impl LdapDirectory {
if principal.verify_secret(secret).await? {
principal
} else {
tracing::debug!(
context = "directory",
event = "invalid_password",
protocol = "ldap",
account = username,
"Invalid password for account"
);
return Ok(None);
}
} else {
@@ -128,8 +128,10 @@ impl LdapDirectory {
"objectClass=*",
&self.mappings.attr_name,
)
.await?
.success()?;
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.success()
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
for entry in rs {
'outer: for (attr, value) in SearchEntry::construct(entry).attrs {
if self.mappings.attr_name.contains(&attr) {
@@ -156,20 +158,23 @@ impl LdapDirectory {
}
}
pub async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
pub async fn email_to_ids(&self, address: &str) -> trc::Result<Vec<u32>> {
let rs = self
.pool
.get()
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.search(
&self.mappings.base_dn,
Scope::Subtree,
&self.mappings.filter_email.build(address.as_ref()),
&self.mappings.attr_name,
)
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.success()
.map(|(rs, _res)| rs)?;
.map(|(rs, _res)| rs)
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
let mut ids = Vec::with_capacity(rs.len());
for entry in rs {
@@ -187,38 +192,46 @@ impl LdapDirectory {
Ok(ids)
}
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
self.pool
.get()
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self.mappings.filter_email.build(address.as_ref()),
&self.mappings.attr_email_address,
)
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.next()
.await
.map(|entry| entry.is_some())
.map_err(|e| e.into())
.map_err(|err| err.into_error().caused_by(trc::location!()))
}
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
let mut stream = self
.pool
.get()
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self.mappings.filter_verify.build(address),
&self.mappings.attr_email_address,
)
.await?;
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
let mut emails = Vec::new();
while let Some(entry) = stream.next().await? {
while let Some(entry) = stream
.next()
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
{
let entry = SearchEntry::construct(entry);
for attr in &self.mappings.attr_email_address {
if let Some(values) = entry.attrs.get(attr) {
@@ -234,21 +247,27 @@ impl LdapDirectory {
Ok(emails)
}
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
let mut stream = self
.pool
.get()
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self.mappings.filter_expand.build(address),
&self.mappings.attr_email_address,
)
.await?;
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
let mut emails = Vec::new();
while let Some(entry) = stream.next().await? {
while let Some(entry) = stream
.next()
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
{
let entry = SearchEntry::construct(entry);
for attr in &self.mappings.attr_email_address {
if let Some(values) = entry.attrs.get(attr) {
@@ -264,21 +283,23 @@ impl LdapDirectory {
Ok(emails)
}
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
self.pool
.get()
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.streaming_search(
&self.mappings.base_dn,
Scope::Subtree,
&self.mappings.filter_domains.build(domain),
Vec::<String>::new(),
)
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.next()
.await
.map(|entry| entry.is_some())
.map_err(|e| e.into())
.map_err(|err| err.into_error().caused_by(trc::location!()))
}
}
@@ -287,14 +308,15 @@ impl LdapDirectory {
&self,
conn: &mut Ldap,
filter: &str,
) -> crate::Result<Option<Principal<String>>> {
) -> trc::Result<Option<Principal<String>>> {
conn.search(
&self.mappings.base_dn,
Scope::Subtree,
filter,
&self.mappings.attrs_principal,
)
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.success()
.map(|(rs, _)| {
rs.into_iter().next().map(|entry| {
@@ -302,7 +324,7 @@ impl LdapDirectory {
.entry_to_principal(SearchEntry::construct(entry))
})
})
.map_err(Into::into)
.map_err(|err| err.into_error().caused_by(trc::location!()))
}
}
@@ -310,12 +332,7 @@ impl LdapMappings {
fn entry_to_principal(&self, entry: SearchEntry) -> Principal<String> {
let mut principal = Principal::default();
tracing::debug!(
context = "ldap",
event = "fetch_principal",
entry = ?entry,
"LDAP entry"
);
trc::trace!(LdapQuery, Value = format!("{entry:?}"));
for (attr, value) in entry.attrs {
if self.attr_name.contains(&attr) {

View File

@@ -11,7 +11,7 @@ use crate::{Principal, QueryBy};
use super::{EmailType, MemoryDirectory};
impl MemoryDirectory {
pub async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
match by {
QueryBy::Name(name) => {
for principal in &self.principals {
@@ -48,7 +48,7 @@ impl MemoryDirectory {
Ok(None)
}
pub async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
pub async fn email_to_ids(&self, address: &str) -> trc::Result<Vec<u32>> {
Ok(self
.emails_to_ids
.get(address)
@@ -65,11 +65,11 @@ impl MemoryDirectory {
.unwrap_or_default())
}
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
Ok(self.emails_to_ids.contains_key(address))
}
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
let mut result = Vec::new();
for (key, value) in &self.emails_to_ids {
if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) {
@@ -79,7 +79,7 @@ impl MemoryDirectory {
Ok(result)
}
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
let mut result = Vec::new();
for (key, value) in &self.emails_to_ids {
if key == address {
@@ -100,7 +100,7 @@ impl MemoryDirectory {
Ok(result)
}
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
Ok(self.domains.contains(domain))
}
}

View File

@@ -7,36 +7,52 @@
use mail_send::{smtp::AssertReply, Credentials};
use smtp_proto::Severity;
use crate::{DirectoryError, Principal, QueryBy};
use crate::{IntoError, Principal, QueryBy};
use super::{SmtpClient, SmtpDirectory};
impl SmtpDirectory {
pub async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
if let QueryBy::Credentials(credentials) = query {
self.pool.get().await?.authenticate(credentials).await
self.pool
.get()
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.authenticate(credentials)
.await
} else {
Err(DirectoryError::unsupported("smtp", "query"))
Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Smtp))
}
}
pub async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
Err(DirectoryError::unsupported("smtp", "email_to_ids"))
pub async fn email_to_ids(&self, _address: &str) -> trc::Result<Vec<u32>> {
Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Smtp))
}
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
let mut conn = self.pool.get().await?;
pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
let mut conn = self
.pool
.get()
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
if !conn.sent_mail_from {
conn.client
.cmd(b"MAIL FROM:<>\r\n")
.await?
.assert_positive_completion()?;
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.assert_positive_completion()
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
conn.sent_mail_from = true;
}
let reply = conn
.client
.cmd(format!("RCPT TO:<{address}>\r\n").as_bytes())
.await?;
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
match reply.severity() {
Severity::PositiveCompletion => {
conn.num_rcpts += 1;
@@ -48,27 +64,32 @@ impl SmtpDirectory {
Ok(true)
}
Severity::PermanentNegativeCompletion => Ok(false),
_ => Err(mail_send::Error::UnexpectedReply(reply).into()),
_ => Err(trc::Cause::Unexpected
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.ctx(trc::Key::Code, reply.code())
.ctx(trc::Key::Details, reply.message)),
}
}
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
self.pool
.get()
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.expand(&format!("VRFY {address}\r\n"))
.await
}
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
self.pool
.get()
.await?
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.expand(&format!("EXPN {address}\r\n"))
.await
}
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
Ok(self.domains.contains(domain))
}
}
@@ -77,7 +98,7 @@ impl SmtpClient {
async fn authenticate(
&mut self,
credentials: &Credentials<String>,
) -> crate::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal<u32>>> {
match self
.client
.authenticate(credentials, &self.capabilities)
@@ -89,21 +110,30 @@ impl SmtpClient {
self.num_auth_failures += 1;
Ok(None)
}
_ => Err(err.into()),
_ => Err(err.into_error()),
},
}
}
async fn expand(&mut self, command: &str) -> crate::Result<Vec<String>> {
let reply = self.client.cmd(command.as_bytes()).await?;
async fn expand(&mut self, command: &str) -> trc::Result<Vec<String>> {
let reply = self
.client
.cmd(command.as_bytes())
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
match reply.code() {
250 | 251 => Ok(reply
.message()
.split('\n')
.map(|p| p.to_string())
.collect::<Vec<String>>()),
550 | 551 | 553 | 500 | 502 => Err(DirectoryError::Unsupported),
_ => Err(mail_send::Error::UnexpectedReply(reply).into()),
code @ (550 | 551 | 553 | 500 | 502) => Err(trc::Cause::Unsupported
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.ctx(trc::Key::Code, code)),
code => Err(trc::Cause::Unexpected
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.ctx(trc::Key::Code, code)
.ctx(trc::Key::Details, reply.message)),
}
}
}

View File

@@ -6,6 +6,7 @@
use mail_send::Credentials;
use store::{NamedRows, Rows, Value};
use trc::AddContext;
use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
@@ -16,7 +17,7 @@ impl SqlDirectory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal<u32>>> {
let mut account_id = None;
let account_name;
let mut secret = None;
@@ -27,10 +28,16 @@ impl SqlDirectory {
self.store
.query::<NamedRows>(&self.mappings.query_name, vec![username.into()])
.await?
.await
.caused_by(trc::location!())?
}
QueryBy::Id(uid) => {
if let Some(username) = self.data_store.get_account_name(uid).await? {
if let Some(username) = self
.data_store
.get_account_name(uid)
.await
.caused_by(trc::location!())?
{
account_name = username;
} else {
return Ok(None);
@@ -42,7 +49,8 @@ impl SqlDirectory {
&self.mappings.query_name,
vec![account_name.clone().into()],
)
.await?
.await
.caused_by(trc::location!())?
}
QueryBy::Credentials(credentials) => {
let (username, secret_) = match credentials {
@@ -55,7 +63,8 @@ impl SqlDirectory {
self.store
.query::<NamedRows>(&self.mappings.query_name, vec![username.into()])
.await?
.await
.caused_by(trc::location!())?
}
};
@@ -64,18 +73,18 @@ impl SqlDirectory {
}
// Map row to principal
let mut principal = self.mappings.row_to_principal(result)?;
let mut principal = self
.mappings
.row_to_principal(result)
.caused_by(trc::location!())?;
// Validate password
if let Some(secret) = secret {
if !principal.verify_secret(secret).await? {
tracing::debug!(
context = "directory",
event = "invalid_password",
protocol = "sql",
account = account_name,
"Invalid password for account"
);
if !principal
.verify_secret(secret)
.await
.caused_by(trc::location!())?
{
return Ok(None);
}
}
@@ -87,7 +96,8 @@ impl SqlDirectory {
principal.id = self
.data_store
.get_or_create_account_id(&account_name)
.await?;
.await
.caused_by(trc::location!())?;
}
principal.name = account_name;
@@ -99,13 +109,17 @@ impl SqlDirectory {
&self.mappings.query_members,
vec![principal.name.clone().into()],
)
.await?
.await
.caused_by(trc::location!())?
.rows
{
if let Some(Value::Text(account_id)) = row.values.first() {
principal
.member_of
.push(self.data_store.get_or_create_account_id(account_id).await?);
principal.member_of.push(
self.data_store
.get_or_create_account_id(account_id)
.await
.caused_by(trc::location!())?,
);
}
}
}
@@ -118,31 +132,38 @@ impl SqlDirectory {
&self.mappings.query_emails,
vec![principal.name.clone().into()],
)
.await?
.await
.caused_by(trc::location!())?
.into();
}
Ok(Some(principal))
}
pub async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
pub async fn email_to_ids(&self, address: &str) -> trc::Result<Vec<u32>> {
let names = self
.store
.query::<Rows>(&self.mappings.query_recipients, vec![address.into()])
.await?;
.await
.caused_by(trc::location!())?;
let mut ids = Vec::with_capacity(names.rows.len());
for row in names.rows {
if let Some(Value::Text(name)) = row.values.first() {
ids.push(self.data_store.get_or_create_account_id(name).await?);
ids.push(
self.data_store
.get_or_create_account_id(name)
.await
.caused_by(trc::location!())?,
);
}
}
Ok(ids)
}
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> {
pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
self.store
.query::<bool>(
&self.mappings.query_recipients,
@@ -152,7 +173,7 @@ impl SqlDirectory {
.map_err(Into::into)
}
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
self.store
.query::<Rows>(
&self.mappings.query_verify,
@@ -163,7 +184,7 @@ impl SqlDirectory {
.map_err(Into::into)
}
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
self.store
.query::<Rows>(
&self.mappings.query_expand,
@@ -174,7 +195,7 @@ impl SqlDirectory {
.map_err(Into::into)
}
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
self.store
.query::<bool>(&self.mappings.query_domains, vec![domain.into()])
.await
@@ -183,7 +204,7 @@ impl SqlDirectory {
}
impl SqlMappings {
pub fn row_to_principal(&self, rows: NamedRows) -> crate::Result<Principal<u32>> {
pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal<u32>> {
let mut principal = Principal::default();
if let Some(row) = rows.rows.into_iter().next() {

View File

@@ -41,7 +41,6 @@ impl Directories {
.property_or_default::<bool>(("directory", id, "disable"), "false")
.unwrap_or(false)
{
tracing::debug!("Skipping disabled directory {id:?}.");
continue;
}
}
@@ -104,7 +103,7 @@ pub(crate) fn build_pool<M: Manager>(
config: &mut Config,
prefix: &str,
manager: M,
) -> utils::config::Result<Pool<M>> {
) -> Result<Pool<M>, String> {
Pool::builder(manager)
.runtime(Runtime::Tokio1)
.max_size(

View File

@@ -4,6 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use trc::AddContext;
use crate::{
backend::internal::lookup::DirectoryStore, Directory, DirectoryInner, Principal, QueryBy,
};
@@ -13,7 +15,7 @@ impl Directory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal<u32>>> {
match &self.store {
DirectoryInner::Internal(store) => store.query(by, return_member_of).await,
DirectoryInner::Ldap(store) => store.query(by, return_member_of).await,
@@ -22,9 +24,10 @@ impl Directory {
DirectoryInner::Smtp(store) => store.query(by).await,
DirectoryInner::Memory(store) => store.query(by).await,
}
.caused_by( trc::location!())
}
pub async fn email_to_ids(&self, email: &str) -> crate::Result<Vec<u32>> {
pub async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>> {
match &self.store {
DirectoryInner::Internal(store) => store.email_to_ids(email).await,
DirectoryInner::Ldap(store) => store.email_to_ids(email).await,
@@ -33,9 +36,10 @@ impl Directory {
DirectoryInner::Smtp(store) => store.email_to_ids(email).await,
DirectoryInner::Memory(store) => store.email_to_ids(email).await,
}
.caused_by( trc::location!())
}
pub async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
pub async fn is_local_domain(&self, domain: &str) -> trc::Result<bool> {
// Check cache
if let Some(cache) = &self.cache {
if let Some(result) = cache.get_domain(domain) {
@@ -50,7 +54,8 @@ impl Directory {
DirectoryInner::Imap(store) => store.is_local_domain(domain).await,
DirectoryInner::Smtp(store) => store.is_local_domain(domain).await,
DirectoryInner::Memory(store) => store.is_local_domain(domain).await,
}?;
}
.caused_by( trc::location!())?;
// Update cache
if let Some(cache) = &self.cache {
@@ -60,7 +65,7 @@ impl Directory {
Ok(result)
}
pub async fn rcpt(&self, email: &str) -> crate::Result<bool> {
pub async fn rcpt(&self, email: &str) -> trc::Result<bool> {
// Check cache
if let Some(cache) = &self.cache {
if let Some(result) = cache.get_rcpt(email) {
@@ -75,7 +80,8 @@ impl Directory {
DirectoryInner::Imap(store) => store.rcpt(email).await,
DirectoryInner::Smtp(store) => store.rcpt(email).await,
DirectoryInner::Memory(store) => store.rcpt(email).await,
}?;
}
.caused_by( trc::location!())?;
// Update cache
if let Some(cache) = &self.cache {
@@ -85,7 +91,7 @@ impl Directory {
Ok(result)
}
pub async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
match &self.store {
DirectoryInner::Internal(store) => store.vrfy(address).await,
DirectoryInner::Ldap(store) => store.vrfy(address).await,
@@ -94,9 +100,10 @@ impl Directory {
DirectoryInner::Smtp(store) => store.vrfy(address).await,
DirectoryInner::Memory(store) => store.vrfy(address).await,
}
.caused_by( trc::location!())
}
pub async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
pub async fn expn(&self, address: &str) -> trc::Result<Vec<String>> {
match &self.store {
DirectoryInner::Internal(store) => store.expn(address).await,
DirectoryInner::Ldap(store) => store.expn(address).await,
@@ -105,5 +112,6 @@ impl Directory {
DirectoryInner::Smtp(store) => store.expn(address).await,
DirectoryInner::Memory(store) => store.expn(address).await,
}
.caused_by( trc::location!())
}
}

View File

@@ -19,11 +19,10 @@ use tokio::sync::oneshot;
use totp_rs::TOTP;
use crate::backend::internal::SpecialSecrets;
use crate::DirectoryError;
use crate::Principal;
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
pub async fn verify_secret(&self, mut code: &str) -> crate::Result<bool> {
pub async fn verify_secret(&self, mut code: &str) -> trc::Result<bool> {
let mut totp_token = None;
let mut is_totp_token_missing = false;
let mut is_totp_required = false;
@@ -59,7 +58,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
// Token needs to validate with at least one of the TOTP secrets
is_totp_verified = TOTP::from_url(secret)
.map_err(DirectoryError::InvalidTotpUrl)?
.map_err(|err| trc::Cause::Invalid.reason(err).details(secret.to_string()))?
.check_current(totp_token)
.unwrap_or(false);
}
@@ -67,9 +66,9 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
if let Some((_, app_secret)) =
secret.strip_prefix("$app$").and_then(|s| s.split_once('$'))
{
is_app_authenticated = verify_secret_hash(app_secret, code).await;
is_app_authenticated = verify_secret_hash(app_secret, code).await?;
} else {
is_authenticated = verify_secret_hash(secret, code).await;
is_authenticated = verify_secret_hash(secret, code).await?;
}
}
}
@@ -83,7 +82,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
// Only let the client know if the TOTP code is missing
// if the password is correct
Err(DirectoryError::MissingTotpCode)
Err(trc::Cause::MissingParameter.into_err())
} else {
// Return the TOTP verification status
@@ -97,7 +96,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
if is_totp_verified {
// TOTP URL appeared after password hash in secrets list
for secret in &self.secrets {
if secret.is_password() && verify_secret_hash(secret, code).await {
if secret.is_password() && verify_secret_hash(secret, code).await? {
return Ok(true);
}
}
@@ -108,7 +107,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
}
}
async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> bool {
async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> trc::Result<bool> {
if hashed_secret.starts_with("$argon2")
|| hashed_secret.starts_with("$pbkdf2")
|| hashed_secret.starts_with("$scrypt")
@@ -119,63 +118,49 @@ async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> bool {
tokio::task::spawn_blocking(move || match PasswordHash::new(&hashed_secret) {
Ok(hash) => {
tx.send(
hash.verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], &secret)
.is_ok(),
)
.ok();
tx.send(Ok(hash
.verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], &secret)
.is_ok()))
.ok();
}
Err(_) => {
tracing::warn!(
context = "directory",
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
tx.send(false).ok();
Err(err) => {
tx.send(Err(trc::Cause::Invalid.reason(err).details(hashed_secret)))
.ok();
}
});
match rx.await {
Ok(result) => result,
Err(_) => {
tracing::warn!(context = "directory", event = "error", "Thread join error");
false
}
Err(err) => Err(trc::Cause::Thread.reason(err)),
}
} else if hashed_secret.starts_with("$2") {
// Blowfish crypt
bcrypt::verify(secret, hashed_secret)
Ok(bcrypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$6$") {
// SHA-512 crypt
sha512_crypt::verify(secret, hashed_secret)
Ok(sha512_crypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$5$") {
// SHA-256 crypt
sha256_crypt::verify(secret, hashed_secret)
Ok(sha256_crypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$sha1") {
// SHA-1 crypt
sha1_crypt::verify(secret, hashed_secret)
Ok(sha1_crypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$1") {
// MD5 based hash
md5_crypt::verify(secret, hashed_secret)
Ok(md5_crypt::verify(secret, hashed_secret))
} else {
// Unknown hash
tracing::warn!(
context = "directory",
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
false
Err(trc::Cause::Invalid
.into_err()
.details(hashed_secret.to_string()))
}
}
pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> trc::Result<bool> {
if hashed_secret.starts_with('$') {
verify_hash_prefix(hashed_secret, secret).await
} else if hashed_secret.starts_with('_') {
// Enhanced DES-based hash
bsdi_crypt::verify(secret, hashed_secret)
Ok(bsdi_crypt::verify(secret, hashed_secret))
} else if let Some(hashed_secret) = hashed_secret.strip_prefix('{') {
if let Some((algo, hashed_secret)) = hashed_secret.split_once('}') {
match algo {
@@ -186,9 +171,13 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
// SHA-1
let mut hasher = Sha1::new();
hasher.update(secret.as_bytes());
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
Ok(
String::from_utf8(
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
)
.unwrap()
== hashed_secret
== hashed_secret,
)
}
"SSHA" => {
// Salted SHA-1
@@ -198,15 +187,19 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
let mut hasher = Sha1::new();
hasher.update(secret.as_bytes());
hasher.update(salt);
&hasher.finalize()[..] == hash
Ok(&hasher.finalize()[..] == hash)
}
"SHA256" => {
// Verify hash
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
Ok(
String::from_utf8(
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
)
.unwrap()
== hashed_secret
== hashed_secret,
)
}
"SSHA256" => {
// Salted SHA-256
@@ -216,15 +209,19 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
hasher.update(salt);
&hasher.finalize()[..] == hash
Ok(&hasher.finalize()[..] == hash)
}
"SHA512" => {
// SHA-512
let mut hasher = Sha512::new();
hasher.update(secret.as_bytes());
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
Ok(
String::from_utf8(
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
)
.unwrap()
== hashed_secret
== hashed_secret,
)
}
"SSHA512" => {
// Salted SHA-512
@@ -234,43 +231,35 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
let mut hasher = Sha512::new();
hasher.update(secret.as_bytes());
hasher.update(salt);
&hasher.finalize()[..] == hash
Ok(&hasher.finalize()[..] == hash)
}
"MD5" => {
// MD5
let digest = md5::compute(secret.as_bytes());
String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap()
== hashed_secret
Ok(
String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap()
== hashed_secret,
)
}
"CRYPT" | "crypt" => {
if hashed_secret.starts_with('$') {
verify_hash_prefix(hashed_secret, secret).await
} else {
// Unix crypt
unix_crypt::verify(secret, hashed_secret)
Ok(unix_crypt::verify(secret, hashed_secret))
}
}
"PLAIN" | "plain" | "CLEAR" | "clear" => hashed_secret == secret,
_ => {
tracing::warn!(
context = "directory",
event = "error",
algorithm = algo,
"Unsupported password hash algorithm"
);
false
}
"PLAIN" | "plain" | "CLEAR" | "clear" => Ok(hashed_secret == secret),
_ => Err(trc::Cause::Invalid
.ctx(trc::Key::Reason, "Unsupported algorithm")
.details(hashed_secret.to_string())),
}
} else {
tracing::warn!(
context = "directory",
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
false
Err(trc::Cause::Invalid
.into_err()
.details(hashed_secret.to_string()))
}
} else {
hashed_secret == secret
Ok(hashed_secret == secret)
}
}

View File

@@ -5,15 +5,11 @@
*/
use core::cache::CachedDirectory;
use std::{
fmt::{Debug, Display},
sync::Arc,
};
use std::{fmt::Debug, sync::Arc};
use ahash::AHashMap;
use backend::{
imap::{ImapDirectory, ImapError},
internal::PrincipalField,
ldap::LdapDirectory,
memory::MemoryDirectory,
smtp::SmtpDirectory,
@@ -23,7 +19,6 @@ use deadpool::managed::PoolError;
use ldap3::LdapError;
use mail_send::Credentials;
use store::Store;
use totp_rs::TotpUrlError;
pub mod backend;
pub mod core;
@@ -72,30 +67,6 @@ pub enum Type {
Other = 6,
}
#[derive(Debug)]
pub enum DirectoryError {
Ldap(LdapError),
Store(store::Error),
Imap(ImapError),
Smtp(mail_send::Error),
Pool(String),
Management(ManagementError),
TimedOut,
Unsupported,
InvalidTotpUrl(TotpUrlError),
MissingTotpCode,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ManagementError {
MissingField(PrincipalField),
AlreadyExists {
field: PrincipalField,
value: String,
},
NotFound(String),
}
pub enum DirectoryInner {
Internal(Store),
Ldap(LdapDirectory),
@@ -158,38 +129,6 @@ pub struct Directories {
pub directories: AHashMap<String, Arc<Directory>>,
}
pub type Result<T> = std::result::Result<T, DirectoryError>;
impl From<PoolError<LdapError>> for DirectoryError {
fn from(error: PoolError<LdapError>) -> Self {
match error {
PoolError::Backend(error) => error.into(),
PoolError::Timeout(_) => DirectoryError::timeout("ldap"),
error => DirectoryError::Pool(error.to_string()),
}
}
}
impl From<PoolError<ImapError>> for DirectoryError {
fn from(error: PoolError<ImapError>) -> Self {
match error {
PoolError::Backend(error) => error.into(),
PoolError::Timeout(_) => DirectoryError::timeout("imap"),
error => DirectoryError::Pool(error.to_string()),
}
}
}
impl From<PoolError<mail_send::Error>> for DirectoryError {
fn from(error: PoolError<mail_send::Error>) -> Self {
match error {
PoolError::Backend(error) => error.into(),
PoolError::Timeout(_) => DirectoryError::timeout("smtp"),
error => DirectoryError::Pool(error.to_string()),
}
}
}
impl Principal<u32> {
pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
Principal {
@@ -211,109 +150,70 @@ impl<T: Ord> Principal<T> {
}
}
impl From<LdapError> for DirectoryError {
fn from(error: LdapError) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "ldap",
reason = %error,
"LDAP directory error"
);
DirectoryError::Ldap(error)
}
trait IntoError {
fn into_error(self) -> trc::Error;
}
impl From<store::Error> for DirectoryError {
fn from(error: store::Error) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "store",
reason = %error,
"Directory error"
);
DirectoryError::Store(error)
}
}
impl From<ImapError> for DirectoryError {
fn from(error: ImapError) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "imap",
reason = %error,
"IMAP directory error"
);
DirectoryError::Imap(error)
}
}
impl From<mail_send::Error> for DirectoryError {
fn from(error: mail_send::Error) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = "smtp",
reason = %error,
"SMTP directory error"
);
DirectoryError::Smtp(error)
}
}
impl DirectoryError {
pub fn unsupported(protocol: &str, method: &str) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = protocol,
method = method,
"Method not supported by directory"
);
DirectoryError::Unsupported
}
pub fn timeout(protocol: &str) -> Self {
tracing::warn!(
context = "directory",
event = "error",
protocol = protocol,
"Directory timed out"
);
DirectoryError::TimedOut
}
}
impl PartialEq for DirectoryError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Store(l0), Self::Store(r0)) => l0 == r0,
(Self::Pool(l0), Self::Pool(r0)) => l0 == r0,
(Self::Management(l0), Self::Management(r0)) => l0 == r0,
_ => false,
}
}
}
impl Display for DirectoryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl IntoError for PoolError<LdapError> {
fn into_error(self) -> trc::Error {
match self {
Self::Ldap(error) => write!(f, "LDAP error: {}", error),
Self::Store(error) => write!(f, "Store error: {}", error),
Self::Imap(error) => write!(f, "IMAP error: {}", error),
Self::Smtp(error) => write!(f, "SMTP error: {}", error),
Self::Pool(error) => write!(f, "Pool error: {}", error),
Self::Management(error) => write!(f, "Management error: {:?}", error),
Self::TimedOut => write!(f, "Directory timed out"),
Self::Unsupported => write!(f, "Method not supported by directory"),
Self::InvalidTotpUrl(error) => write!(f, "Invalid TOTP URL: {}", error),
Self::MissingTotpCode => write!(f, "Missing TOTP code"),
PoolError::Backend(error) => error.into_error(),
PoolError::Timeout(_) => {
trc::Cause::Timeout.ctx(trc::Key::Protocol, trc::Protocol::Ldap)
}
err => trc::Cause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Ldap)
.reason(err),
}
}
}
impl IntoError for PoolError<ImapError> {
fn into_error(self) -> trc::Error {
match self {
PoolError::Backend(error) => error.into_error(),
PoolError::Timeout(_) => {
trc::Cause::Timeout.ctx(trc::Key::Protocol, trc::Protocol::Imap)
}
err => trc::Cause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Imap)
.reason(err),
}
}
}
impl IntoError for PoolError<mail_send::Error> {
fn into_error(self) -> trc::Error {
match self {
PoolError::Backend(error) => error.into_error(),
PoolError::Timeout(_) => {
trc::Cause::Timeout.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
}
err => trc::Cause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.reason(err),
}
}
}
impl IntoError for ImapError {
fn into_error(self) -> trc::Error {
trc::Cause::Imap.reason(self)
}
}
impl IntoError for mail_send::Error {
fn into_error(self) -> trc::Error {
trc::Cause::Smtp.reason(self)
}
}
impl IntoError for LdapError {
fn into_error(self) -> trc::Error {
if let LdapError::LdapResult { result } = &self {
trc::Cause::Ldap.ctx(trc::Key::Code, result.rc).reason(self)
} else {
trc::Cause::Ldap.reason(self)
}
}
}